Orderbook Complete Guide (Proxy Wallet)
This comprehensive guide walks you through the complete process of interacting with Probable Markets orderbook using a proxy wallet (Gnosis Safe). This step-by-step tutorial covers all prerequisites, setup, and the complete order placement flow.
Alternative Flow: If you prefer to use your EOA wallet directly without creating a proxy wallet, see the Orderbook EOA Guide for a simpler integration flow.
Overview
This guide provides a complete walkthrough for integrating with Probable Markets orderbook, starting from scratch with just an EOA wallet. You'll learn how to:
- Create a Proxy Wallet - Set up a Gnosis Safe smart contract wallet derived from your EOA
- Approve Tokens - Grant spending allowances for USDT and CTF tokens
- Deposit USDT - Transfer USDT to your proxy wallet
- Create API Key - Generate API credentials for authenticated requests
- Create and Sign Order - Build an EIP-712 signed order
- Submit Order - Send the order to the API with L2 authentication
Note: This guide references the implementation in
clob-examples. For complete working code, see the clob-examples repository.
Prerequisites
- An EOA wallet with:
- BNB for transaction fees
- USDT for trading (will be deposited to proxy wallet)
- Access to a BSC RPC endpoint
- Node.js/Bun environment with
viemand@safe-global/protocol-kitinstalled
Contract Addresses
const PROXY_FACTORY_ADDRESS = '0xB99159aBF0bF59a512970586F38292f8b9029924';
const USDT_ADDRESS = '0x55d398326f99059fF775485246999027B3197955';
const CTF_TOKEN_ADDRESS = '0x364d05055614B506e2b9A287E4ac34167204cA83';
const CTF_EXCHANGE_ADDRESS = '0xF99F5367ce708c66F0860B77B4331301A5597c86';
const CHAIN_ID = 56; // BSC MainnetStep 1: Create Proxy Wallet
The proxy wallet is a Gnosis Safe smart contract wallet that acts as the maker for your orders. It's deterministically derived from your EOA address.
1.1 Check if Proxy Exists
First, check if a proxy wallet already exists for your EOA:
import { getContract } from 'viem';
import { PROXY_FACTORY_ADDRESS, PROXY_FACTORY_ABI } from './config';
async function checkProxyExists(userAddress: `0x${string}`) {
const proxyFactory = getContract({
address: PROXY_FACTORY_ADDRESS,
abi: PROXY_FACTORY_ABI,
client: publicClient,
});
const proxyAddress = await proxyFactory.read.computeProxyAddress([userAddress]);
// Check if proxy is deployed
const code = await publicClient.getBytecode({ address: proxyAddress });
const exists = code !== undefined && code !== '0x';
return { exists, address: proxyAddress };
}1.2 Create Proxy Wallet
If the proxy doesn't exist, create it with an EIP-712 signature:
import { PROXY_FACTORY_ABI } from './config';
async function createProxyWallet(
userAddress: `0x${string}`,
walletClient: WalletClient
): Promise<`0x${string}`> {
const proxyFactory = getContract({
address: PROXY_FACTORY_ADDRESS,
abi: PROXY_FACTORY_ABI,
client: { public: publicClient, wallet: walletClient },
});
// Get computed proxy address
const proxyAddress = await proxyFactory.read.computeProxyAddress([userAddress]);
// Generate EIP-712 signature for CreateProxy
const paymentToken = '0x0000000000000000000000000000000000000000';
const payment = 0n;
const paymentReceiver = '0x0000000000000000000000000000000000000000';
const domain = {
name: 'Probable Contract Proxy Factory',
chainId: 56,
verifyingContract: PROXY_FACTORY_ADDRESS,
};
const types = {
CreateProxy: [
{ name: 'paymentToken', type: 'address' },
{ name: 'payment', type: 'uint256' },
{ name: 'paymentReceiver', type: 'address' },
],
};
const message = { paymentToken, payment, paymentReceiver };
const signature = await walletClient.signTypedData({
domain,
types,
primaryType: 'CreateProxy',
message,
});
// Parse signature into v, r, s
const r = `0x${signature.slice(2, 66)}` as `0x${string}`;
const s = `0x${signature.slice(66, 130)}` as `0x${string}`;
let v = parseInt(signature.slice(130, 132), 16);
if (v < 27) v += 27;
// Create proxy
const hash = await proxyFactory.write.createProxy(
[paymentToken, payment, paymentReceiver, { v, r, s }],
{ account: walletClient.account }
);
await publicClient.waitForTransactionReceipt({ hash });
return proxyAddress;
}Step 2: Approve Tokens
Before placing orders, you must approve token spending. The proxy wallet needs three approvals:
- USDT → CTF Token Contract (for splitting/merging positions)
- USDT → CTF Exchange (for trading)
- CTF Tokens → CTF Exchange (for trading)
2.1 Check Existing Approvals
import { maxUint256 } from 'viem';
import { ERC20_ABI, ERC1155_ABI } from './config';
async function checkApprovals(proxyAddress: `0x${string}`) {
const [usdtForCTFToken, usdtForExchange, ctfTokenForExchange] =
await publicClient.multicall({
contracts: [
{
address: USDT_ADDRESS,
abi: ERC20_ABI,
functionName: 'allowance',
args: [proxyAddress, CTF_TOKEN_ADDRESS],
},
{
address: USDT_ADDRESS,
abi: ERC20_ABI,
functionName: 'allowance',
args: [proxyAddress, CTF_EXCHANGE_ADDRESS],
},
{
address: CTF_TOKEN_ADDRESS,
abi: ERC1155_ABI,
functionName: 'isApprovedForAll',
args: [proxyAddress, CTF_EXCHANGE_ADDRESS],
},
],
});
return {
needsUSDTForCTFToken: usdtForCTFToken.result < maxUint256,
needsUSDTForExchange: usdtForExchange.result < maxUint256,
needsCTFTokenForExchange: !ctfTokenForExchange.result,
};
}2.2 Execute Approvals via Safe
All approvals are executed through the proxy wallet (Gnosis Safe) in a single batch transaction:
import Safe from '@safe-global/protocol-kit';
import { encodeFunctionData } from 'viem';
async function approveTokens(
proxyAddress: `0x${string}`,
privateKey: `0x${string}`
) {
const protocolKit = await Safe.init({
provider: rpcUrl,
signer: privateKey,
safeAddress: proxyAddress,
});
const transactions = [];
// USDT approval for CTF Token
transactions.push({
to: USDT_ADDRESS,
data: encodeFunctionData({
abi: ERC20_ABI,
functionName: 'approve',
args: [CTF_TOKEN_ADDRESS, maxUint256],
}) as `0x${string}`,
value: '0',
});
// USDT approval for Exchange
transactions.push({
to: USDT_ADDRESS,
data: encodeFunctionData({
abi: ERC20_ABI,
functionName: 'approve',
args: [CTF_EXCHANGE_ADDRESS, maxUint256],
}) as `0x${string}`,
value: '0',
});
// CTF Token approval for Exchange
transactions.push({
to: CTF_TOKEN_ADDRESS,
data: encodeFunctionData({
abi: ERC1155_ABI,
functionName: 'setApprovalForAll',
args: [CTF_EXCHANGE_ADDRESS, true],
}) as `0x${string}`,
value: '0',
});
const safeTransaction = await protocolKit.createTransaction({
transactions,
});
const executeTxResponse = await protocolKit.executeTransaction(safeTransaction);
await publicClient.waitForTransactionReceipt({ hash: executeTxResponse.hash });
}Step 3: Deposit USDT to Proxy Wallet
After approvals, deposit USDT from your EOA to the proxy wallet:
async function depositUSDT(
eoaAddress: `0x${string}`,
proxyAddress: `0x${string}`,
amount: bigint,
walletClient: WalletClient
) {
const hash = await walletClient.sendTransaction({
to: USDT_ADDRESS,
data: encodeFunctionData({
abi: ERC20_ABI,
functionName: 'transfer',
args: [proxyAddress, amount],
}),
account: walletClient.account,
});
await publicClient.waitForTransactionReceipt({ hash });
}Step 4: Create API Key
API keys are required for authenticated requests. They're created using L1 authentication (EIP-712 signature).
4.1 Build L1 Authentication Signature
async function buildL1Signature(
walletClient: WalletClient,
chainId: number,
timestamp: number,
nonce: number
): Promise<string> {
const eoaAddress = walletClient.account.address;
const MSG_TO_SIGN = "This message attests that I control the given wallet";
const domain = {
name: 'ClobAuthDomain',
version: '1',
chainId: chainId,
};
const types = {
ClobAuth: [
{ name: 'address', type: 'address' },
{ name: 'timestamp', type: 'string' },
{ name: 'nonce', type: 'uint256' },
{ name: 'message', type: 'string' },
],
};
const message = {
address: eoaAddress,
timestamp: timestamp.toString(),
nonce: nonce,
message: MSG_TO_SIGN,
};
return await walletClient.signTypedData({
domain,
types,
primaryType: 'ClobAuth',
message,
});
}4.2 Get Nonce and Create API Key
import axios from 'axios';
async function createApiKey(walletClient: WalletClient): Promise<{
key: string;
secret: string;
passphrase: string;
}> {
const eoaAddress = walletClient.account.address;
const timestamp = Math.floor(Date.now() / 1000);
// Get nonce (optional, can use 0)
const nonce = 0;
// Build signature
const signature = await buildL1Signature(
walletClient,
CHAIN_ID,
timestamp,
nonce
);
// Create API key
const response = await axios.post(
`https://api.probable.markets/public/api/v1/auth/api-key/${CHAIN_ID}`,
{},
{
headers: {
prob_address: eoaAddress,
prob_signature: signature,
prob_timestamp: timestamp.toString(),
prob_nonce: nonce.toString(),
},
}
);
return {
key: response.data.apiKey,
secret: response.data.secret,
passphrase: response.data.passphrase,
};
}Important: Store the
secretandpassphrasesecurely. They cannot be retrieved later.
Step 5: Create and Sign Order
Orders are created using EIP-712 typed data signing. The order structure includes maker (proxy wallet), signer (EOA), token ID, amounts, and other parameters.
5.1 Build Order Data
import { parseUnits } from 'viem';
interface UserOrder {
tokenID: string;
price: number;
size: number;
side: 0 | 1; // 0 = BUY, 1 = SELL
feeRateBps?: number; // Max taker fee in basis points (175-1000)
nonce?: number;
}
// Helper functions for rounding
function roundDown(num: number, decimals: number): number {
if (decimalPlaces(num) <= decimals) return num;
return Math.floor(num * 10 ** decimals) / 10 ** decimals;
}
function roundUp(num: number, decimals: number): number {
if (decimalPlaces(num) <= decimals) return num;
return Math.ceil(num * 10 ** decimals) / 10 ** decimals;
}
function roundNormal(num: number, decimals: number): number {
if (decimalPlaces(num) <= decimals) return num;
return Math.round((num + Number.EPSILON) * 10 ** decimals) / 10 ** decimals;
}
function decimalPlaces(num: number): number {
if (Number.isInteger(num)) return 0;
const arr = num.toString().split('.');
if (arr.length <= 1) return 0;
return arr[1]!.length;
}
// Rounding config based on tick size
const ROUNDING_CONFIG = {
'0.1': { price: 1, size: 2, amount: 3 },
'0.01': { price: 2, size: 2, amount: 4 },
'0.001': { price: 3, size: 2, amount: 5 },
'0.0001': { price: 4, size: 2, amount: 6 },
};
function calculateOrderAmounts(
side: 0 | 1,
size: number,
price: number,
tickSize: '0.01' | '0.1' | '0.001' | '0.0001'
): { rawMakerAmt: number; rawTakerAmt: number } {
const roundConfig = ROUNDING_CONFIG[tickSize];
const rawPrice = roundNormal(price, roundConfig.price);
if (side === 0) { // BUY
const rawTakerAmt = roundDown(size, roundConfig.size);
let rawMakerAmt = rawTakerAmt * rawPrice;
if (decimalPlaces(rawMakerAmt) > roundConfig.amount) {
rawMakerAmt = roundUp(rawMakerAmt, roundConfig.amount + 4);
if (decimalPlaces(rawMakerAmt) > roundConfig.amount) {
rawMakerAmt = roundDown(rawMakerAmt, roundConfig.amount);
}
}
return { rawMakerAmt, rawTakerAmt };
} else { // SELL
const rawMakerAmt = roundDown(size, roundConfig.size);
let rawTakerAmt = rawMakerAmt * rawPrice;
if (decimalPlaces(rawTakerAmt) > roundConfig.amount) {
rawTakerAmt = roundUp(rawTakerAmt, roundConfig.amount + 4);
if (decimalPlaces(rawTakerAmt) > roundConfig.amount) {
rawTakerAmt = roundDown(rawTakerAmt, roundConfig.amount);
}
}
return { rawMakerAmt, rawTakerAmt };
}
}
function buildOrderData(
userOrder: UserOrder,
eoaAddress: string,
proxyAddress: string,
tickSize: '0.01' | '0.1' | '0.001' | '0.0001'
): {
maker: string;
signer: string;
tokenId: string;
makerAmount: string;
takerAmount: string;
side: number;
feeRateBps: string;
nonce: string;
expiration: string;
signatureType: number;
} {
// Calculate raw amounts based on side
const { rawMakerAmt, rawTakerAmt } = calculateOrderAmounts(
userOrder.side,
userOrder.size,
userOrder.price,
tickSize
);
const makerAmount = parseUnits(
rawMakerAmt.toString(),
18 // USDT decimals
).toString();
const takerAmount = parseUnits(
rawTakerAmt.toString(),
18
).toString();
const feeRateBps = userOrder.feeRateBps ?? 175;
if (feeRateBps < 175 || feeRateBps > 1000) {
throw new Error('feeRateBps must be between 175 and 1000 (inclusive)');
}
return {
maker: proxyAddress, // Use proxy wallet as maker
signer: eoaAddress, // Use EOA as signer
tokenId: userOrder.tokenID,
makerAmount,
takerAmount,
side: userOrder.side,
feeRateBps: feeRateBps.toString(),
nonce: (userOrder.nonce || 0).toString(),
expiration: '0', // 0 = no expiration
signatureType: 1, // PROB_GNOSIS_SAFE
};
}5.2 Sign Order with EIP-712
async function signOrder(
orderData: ReturnType<typeof buildOrderData>,
walletClient: WalletClient,
salt: string
): Promise<string> {
const domain = {
name: 'Probable CTF Exchange',
version: '1',
chainId: CHAIN_ID,
verifyingContract: CTF_EXCHANGE_ADDRESS,
};
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' },
{ name: 'signatureType', type: 'uint8' },
],
};
const taker = '0x0000000000000000000000000000000000000000';
const message = {
salt: BigInt(salt),
maker: orderData.maker as `0x${string}`,
signer: orderData.signer as `0x${string}`,
taker: taker as `0x${string}`,
tokenId: BigInt(orderData.tokenId),
makerAmount: BigInt(orderData.makerAmount),
takerAmount: BigInt(orderData.takerAmount),
expiration: BigInt(orderData.expiration),
nonce: BigInt(orderData.nonce),
feeRateBps: BigInt(orderData.feeRateBps),
side: orderData.side,
signatureType: orderData.signatureType,
};
return await walletClient.signTypedData({
domain,
types,
primaryType: 'Order',
message,
});
}Step 6: Submit Order to API
Finally, submit the signed order to the API using L2 authentication (HMAC signature).
6.1 Build L2 HMAC Signature
import * as crypto from 'crypto';
function buildL2Signature(
secret: string,
timestamp: number,
method: string,
path: string,
body: string
): string {
const message = `${timestamp}${method}${path}${body}`;
const base64Secret = Buffer.from(secret, 'base64');
const hmac = crypto.createHmac('sha256', base64Secret);
const sig = hmac.update(message).digest('base64');
// URL-safe base64 encoding
return sig.replace(/\+/g, '-').replace(/\//g, '_');
}6.2 Submit Order
async function submitOrder(
signedOrder: {
salt: string;
maker: string;
signer: string;
taker: string;
tokenId: string;
makerAmount: string;
takerAmount: string;
side: number;
expiration: string;
nonce: string;
feeRateBps: string;
signatureType: number;
signature: string;
},
apiKey: string,
secret: string,
passphrase: string,
eoaAddress: string
) {
const requestBody = {
deferExec: true,
order: {
salt: signedOrder.salt,
maker: signedOrder.maker,
signer: signedOrder.signer,
taker: signedOrder.taker,
tokenId: signedOrder.tokenId,
makerAmount: signedOrder.makerAmount,
takerAmount: signedOrder.takerAmount,
side: signedOrder.side === 0 ? 'BUY' : 'SELL',
expiration: signedOrder.expiration,
nonce: signedOrder.nonce,
feeRateBps: signedOrder.feeRateBps,
signatureType: signedOrder.signatureType,
signature: signedOrder.signature,
},
owner: signedOrder.maker, // Proxy wallet address
orderType: 'GTC', // Good Till Cancel
};
const timestamp = Math.floor(Date.now() / 1000);
const method = 'POST';
const path = `/public/api/v1/order/${CHAIN_ID}`;
const bodyString = JSON.stringify(requestBody);
const signature = buildL2Signature(secret, timestamp, method, path, bodyString);
const response = await axios.post(
`https://api.probable.markets${path}`,
requestBody,
{
headers: {
'Content-Type': 'application/json',
prob_address: eoaAddress, // Use EOA address for authentication
prob_signature: signature,
prob_timestamp: timestamp.toString(),
prob_api_key: apiKey,
prob_passphrase: passphrase,
},
}
);
return response.data;
}Complete Example
Here's a complete example that ties everything together:
async function placeOrderFromEOA(
walletClient: WalletClient,
tokenId: string,
price: number,
size: number,
side: 0 | 1
) {
const eoaAddress = walletClient.account.address;
// Step 1: Create or get proxy wallet
const { exists, address: proxyAddress } = await checkProxyExists(eoaAddress);
if (!exists) {
await createProxyWallet(eoaAddress, walletClient);
}
// Step 2: Approve tokens
const approvals = await checkApprovals(proxyAddress);
if (approvals.needsUSDTForCTFToken ||
approvals.needsUSDTForExchange ||
approvals.needsCTFTokenForExchange) {
await approveTokens(proxyAddress, privateKey);
}
// Step 3: Ensure USDT balance (user should deposit manually)
// Check balance and prompt user if needed
// Step 4: Create API key
const apiKey = await createApiKey(walletClient);
// Step 5: Create and sign order
const userOrder: UserOrder = {
tokenID: tokenId,
price,
size,
side,
feeRateBps: 175,
nonce: 0,
};
const orderData = buildOrderData(userOrder, eoaAddress, proxyAddress, '0.01');
const salt = `${Math.round(Math.random() * Date.now())}`;
const signature = await signOrder(orderData, walletClient, salt);
const signedOrder = {
...orderData,
salt,
taker: '0x0000000000000000000000000000000000000000',
signature,
};
// Step 6: Submit order
const result = await submitOrder(
signedOrder,
apiKey.key,
apiKey.secret,
apiKey.passphrase,
eoaAddress
);
console.log('Order placed:', result);
return result;
}Key Points to Remember
-
Address Usage:
- Authentication headers (
prob_address): Always use your EOA address - Order
makerfield: Use your proxy wallet address - Order
signerfield: Use your EOA address (signs the order) - Order
ownerfield: Use your proxy wallet address
- Authentication headers (
-
Signature Types:
- L1 authentication: EIP-712 signature with EOA
- Order signing: EIP-712 signature with EOA (signatureType = 1 for PROB_GNOSIS_SAFE)
- L2 API requests: HMAC-SHA256 signature with API secret
-
Token Approvals:
- All approvals are executed through the proxy wallet (Gnosis Safe)
- Approvals can be batched in a single Safe transaction
-
Order Parameters:
tickSize: Determines price precision ('0.01', '0.1', '0.001', or '0.0001')deferExec: Set totrueto defer executionfeeRateBps: Max taker fee in basis points (175-1000)orderType: 'GTC' (Good Till Cancel) or 'IOC' (Immediate Or Cancel)
Next Steps
- Orderbook API Overview - Complete API reference
- Orders API - Order management endpoints
- Proxy Wallet Guide - Detailed proxy wallet documentation
- Authentication Guide - Authentication details