Complete Guide: Orderbook Integration with EOA Wallet
This comprehensive guide walks you through the complete process of interacting with Probable Markets orderbook using your EOA (Externally Owned Account) wallet directly, without creating a proxy wallet. This is a simpler flow that uses your wallet directly for all operations.
Overview
This guide provides a complete walkthrough for integrating with Probable Markets orderbook using your EOA wallet directly. You'll learn how to:
- Approve Tokens - Grant spending allowances for USDT and CTF tokens directly from your EOA
- Check Balance - Verify your EOA has sufficient USDT
- Create API Key - Generate API credentials for authenticated requests
- Create and Sign Order - Build an EIP-712 signed order with EOA signature
- 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. Runbun run index-eoa.tsto execute the EOA flow.
Prerequisites
- An EOA wallet with:
- BNB for transaction fees
- USDT for trading (in your EOA wallet)
- Access to a BSC RPC endpoint
- Node.js/Bun environment with
vieminstalled
Key Difference: Unlike the proxy wallet flow, this guide uses your EOA directly - no proxy wallet creation needed!
Contract Addresses
const USDT_ADDRESS = '0x55d398326f99059fF775485246999027B3197955';
const CTF_TOKEN_ADDRESS = '0x364d05055614B506e2b9A287E4ac34167204cA83';
const CTF_EXCHANGE_ADDRESS = '0xF99F5367ce708c66F0860B77B4331301A5597c86';
const CHAIN_ID = 56; // BSC MainnetStep 1: Approve Tokens
Unlike the proxy wallet flow, token approvals are done directly from your EOA using standard ERC20/ERC1155 transactions. You need three approvals:
- USDT → CTF Token Contract (for splitting/merging positions)
- USDT → CTF Exchange (for trading)
- CTF Tokens → CTF Exchange (for trading)
1.1 Check Existing Approvals
import { maxUint256 } from 'viem';
import { getContract } from 'viem';
import { ERC20_ABI, ERC1155_ABI } from './config';
async function checkAllApprovals(eoaAddress: `0x${string}`) {
const results = await publicClient.multicall({
contracts: [
{
address: USDT_ADDRESS,
abi: ERC20_ABI,
functionName: 'allowance',
args: [eoaAddress, CTF_TOKEN_ADDRESS],
},
{
address: USDT_ADDRESS,
abi: ERC20_ABI,
functionName: 'allowance',
args: [eoaAddress, CTF_EXCHANGE_ADDRESS],
},
{
address: CTF_TOKEN_ADDRESS,
abi: ERC1155_ABI,
functionName: 'isApprovedForAll',
args: [eoaAddress, CTF_EXCHANGE_ADDRESS],
},
],
});
return {
needsUSDTForCTFToken: results[0].status === 'success' && results[0].result < maxUint256,
needsUSDTForExchange: results[1].status === 'success' && results[1].result < maxUint256,
needsCTFTokenForExchange: results[2].status === 'success' && !results[2].result,
};
}1.2 Execute Approvals Directly
All approvals are executed as direct transactions from your EOA:
async function approveTokensEOA(eoaAddress: `0x${string}`) {
const { needsUSDTForCTFToken, needsUSDTForExchange, needsCTFTokenForExchange } =
await checkAllApprovals(eoaAddress);
// Approve USDT for CTF Token
if (needsUSDTForCTFToken) {
const usdtContract = getContract({
address: USDT_ADDRESS,
abi: ERC20_ABI,
client: { public: publicClient, wallet: walletClient },
});
const hash = await usdtContract.write.approve([CTF_TOKEN_ADDRESS, maxUint256]);
await publicClient.waitForTransactionReceipt({ hash });
}
// Approve USDT for Exchange
if (needsUSDTForExchange) {
const usdtContract = getContract({
address: USDT_ADDRESS,
abi: ERC20_ABI,
client: { public: publicClient, wallet: walletClient },
});
const hash = await usdtContract.write.approve([CTF_EXCHANGE_ADDRESS, maxUint256]);
await publicClient.waitForTransactionReceipt({ hash });
}
// Approve CTF Tokens for Exchange
if (needsCTFTokenForExchange) {
const ctfTokenContract = getContract({
address: CTF_TOKEN_ADDRESS,
abi: ERC1155_ABI,
client: { public: publicClient, wallet: walletClient },
});
const hash = await ctfTokenContract.write.setApprovalForAll([CTF_EXCHANGE_ADDRESS, true]);
await publicClient.waitForTransactionReceipt({ hash });
}
}Step 2: Check USDT Balance
Verify that your EOA has sufficient USDT for trading:
async function checkUSDTBalanceEOA(eoaAddress: `0x${string}`) {
const usdtContract = getContract({
address: USDT_ADDRESS,
abi: ERC20_ABI,
client: { public: publicClient },
});
const decimals = await usdtContract.read.decimals();
const decimalsMultiplier = 10n ** BigInt(decimals);
const balance = await usdtContract.read.balanceOf([eoaAddress]);
const balanceInUSDT = Number(balance) / Number(decimalsMultiplier);
if (balanceInUSDT <= 1) {
throw new Error('Insufficient USDT balance. Please deposit at least 1 USDT to your EOA.');
}
console.log(`USDT Balance: ${balanceInUSDT.toFixed(6)} USDT`);
return balanceInUSDT;
}Step 3: Create API Key
API keys are created using L1 authentication (EIP-712 signature) with your EOA. The process is the same as the proxy wallet flow, but you use your EOA address directly.
3.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,
});
}3.2 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);
const nonce = 0;
const signature = await buildL1Signature(
walletClient,
CHAIN_ID,
timestamp,
nonce
);
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,
};
}Step 4: Create and Sign Order
Orders are created using EIP-712 typed data signing. For EOA wallets, the key difference is:
- Signature Type:
SignatureType.EOA(instead ofPROB_GNOSIS_SAFE) - Maker: Your EOA address (same as signer)
- Signer: Your EOA address
4.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;
}
function buildOrderData(
userOrder: UserOrder,
eoaAddress: 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 (same as proxy wallet flow)
const { rawMakerAmt, rawTakerAmt } = calculateOrderAmounts(
userOrder.side,
userOrder.size,
userOrder.price,
tickSize
);
const makerAmount = parseUnits(rawMakerAmt.toString(), 18).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: eoaAddress, // EOA is both maker and signer
signer: eoaAddress, // EOA signs the order
tokenId: userOrder.tokenID,
makerAmount,
takerAmount,
side: userOrder.side,
feeRateBps: feeRateBps.toString(),
nonce: (userOrder.nonce || 0).toString(),
expiration: '0',
signatureType: 0, // SignatureType.EOA
};
}4.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 5: Submit Order to API
Submit the signed order to the API using L2 authentication. The key difference is passing accountType: 'eoa' in the API request.
5.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, '_');
}5.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, // EOA address
orderType: 'GTC',
};
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,
prob_signature: signature,
prob_timestamp: timestamp.toString(),
prob_api_key: apiKey,
prob_passphrase: passphrase,
prob_account_type: 'eoa', // Important: specify EOA account type
},
}
);
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: Approve tokens
const approvals = await checkAllApprovals(eoaAddress);
if (approvals.needsUSDTForCTFToken ||
approvals.needsUSDTForExchange ||
approvals.needsCTFTokenForExchange) {
await approveTokensEOA(eoaAddress);
}
// Step 2: Check USDT balance
await checkUSDTBalanceEOA(eoaAddress);
// Step 3: Create API key
const apiKey = await createApiKey(walletClient);
// Step 4: Create and sign order
const userOrder: UserOrder = {
tokenID: tokenId,
price,
size,
side,
feeRateBps: 175,
nonce: 0,
};
const orderData = buildOrderData(userOrder, eoaAddress, '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 5: Submit order
const result = await submitOrder(
signedOrder,
apiKey.key,
apiKey.secret,
apiKey.passphrase,
eoaAddress
);
console.log('Order placed:', result);
return result;
}Key Differences from Proxy Wallet Flow
| Aspect | Proxy Wallet Flow | EOA Flow |
|---|---|---|
| Wallet Setup | Create Gnosis Safe proxy wallet | Use EOA directly |
| Token Approvals | Via Safe transactions (batched) | Direct ERC20/ERC1155 transactions |
| Order Maker | Proxy wallet address | EOA address |
| Order Signer | EOA address | EOA address |
| Signature Type | PROB_GNOSIS_SAFE (1) | EOA (0) |
| API Account Type | Not specified (defaults to proxy) | 'eoa' |
| USDT Location | Proxy wallet | EOA wallet |
| Complexity | Higher (Safe SDK required) | Lower (standard transactions) |
Advantages of EOA Flow
- Simpler Setup - No proxy wallet creation needed
- Lower Gas Costs - Direct transactions are cheaper than Safe transactions
- Faster Execution - No need to wait for proxy wallet deployment
- Direct Control - Full control over your funds in your EOA
When to Use EOA Flow
- You want a simpler integration without proxy wallet complexity
- You're comfortable managing funds directly in your EOA
- You want lower gas costs
- You don't need the additional security features of a Safe wallet
Next Steps
- Orderbook Complete Guide (Proxy Wallet) - Guide for proxy wallet flow with additional security features
- Orderbook API Overview - Complete API reference
- TypeScript SDK - SDK documentation
- Authentication Guide - Authentication details
Choosing Between EOA and Proxy Wallet Flow
Use EOA Flow if:- You want a simpler setup without proxy wallet complexity
- You prefer lower gas costs
- You're comfortable managing funds directly in your EOA
- You don't need Safe wallet features
- You want additional security features (multi-sig, recovery, etc.)
- You prefer to separate trading funds from your main EOA
- You need advanced Safe wallet capabilities
- You want to batch multiple transactions together