Skip to content

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:

  1. Approve Tokens - Grant spending allowances for USDT and CTF tokens directly from your EOA
  2. Check Balance - Verify your EOA has sufficient USDT
  3. Create API Key - Generate API credentials for authenticated requests
  4. Create and Sign Order - Build an EIP-712 signed order with EOA signature
  5. 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. Run bun run index-eoa.ts to 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 viem installed

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 Mainnet

Step 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:

  1. USDT → CTF Token Contract (for splitting/merging positions)
  2. USDT → CTF Exchange (for trading)
  3. 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 of PROB_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

AspectProxy Wallet FlowEOA Flow
Wallet SetupCreate Gnosis Safe proxy walletUse EOA directly
Token ApprovalsVia Safe transactions (batched)Direct ERC20/ERC1155 transactions
Order MakerProxy wallet addressEOA address
Order SignerEOA addressEOA address
Signature TypePROB_GNOSIS_SAFE (1)EOA (0)
API Account TypeNot specified (defaults to proxy)'eoa'
USDT LocationProxy walletEOA wallet
ComplexityHigher (Safe SDK required)Lower (standard transactions)

Advantages of EOA Flow

  1. Simpler Setup - No proxy wallet creation needed
  2. Lower Gas Costs - Direct transactions are cheaper than Safe transactions
  3. Faster Execution - No need to wait for proxy wallet deployment
  4. 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


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
Use Proxy Wallet Flow if:
  • 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