← Workspace Index

Batch Processing Implementation Design

Task-1 Review: Order/Trade Batch Processing Architecture

December 9, 2025

1. Current Architecture Analysis

Order Flow (Limit Orders)

User signs EIP-712 Order | v POST /api/orders/user/create | +---> Save to Database | +---> Attempt Auto-Match | v If match found: Server executes matchOrders()

Trade Flow (Market Orders / Sweeping)

User wants to buy 100 tokens (sweep multiple orders) | v FOR EACH maker order in orderbook: | +---> User signs taker order (EIP-712) <-- Multiple signatures! | +---> POST /api/trade/execute | +---> Server executes matchOrders() <-- Separate TX each time! | v Loop continues until filled

Batch Cancel (Working Example)

User selects multiple orders to cancel | v Prepare multicall data: [cancelOrder, cancelOrder, ...] | v Single TX: CTFExchange.multicall(calls) | v All orders cancelled atomically

2. Issues Identified

Issue #1: Multiple Signatures Required

useTrade.ts:executeTrade() lines 230-260

User must sign each order separately in a sweep. Poor UX - buying 100 tokens from 5 different orders requires 5 MetaMask signature popups.

Issue #2: No Transaction Batching

trade.ts - matchOrders route

Each order match is a separate blockchain transaction. 5 orders = 5 TXs = 5x gas fees. No atomicity - if TX #3 fails, user has partial fill with no easy recovery.

Issue #3: Database Save Timing

orders.ts - order creation

Orders saved to DB before blockchain confirmation. Can cause DB/blockchain state mismatch if TX fails after DB write.

2a. Resolved Critical Issue: Limit Order Matching

Problem: Execution Reverted (NotCrossing)

Legacy floating-point math in seeding caused "dust" pricing (e.g., Ask at $0.51000005) which failed to cross with precise Limit Bids at $0.51, causing the smart contract to revert.

Solution Architecture

3. Current vs Proposed UX Comparison

Aspect Current Proposed Status
User signs order EIP-712 typed data Same OK
Server submits to blockchain Yes (operator wallet) Same OK
Error handling & retry None Auto-retry queue Missing
Signatures per sweep One per order Single signature Missing
Transaction batching Only for cancel All operations Partial
DB synchronization Save before TX Save after confirmation Missing

4. Implementation Recommendations

Priority 1: Transaction Queue with Status Tracking

Add a queue system to track pending transactions and enable retry logic.

// New Prisma model
model TransactionQueue {
  id           String   @id @default(uuid())
  type         String   // 'order' | 'trade' | 'cancel'
  signedData   Json     // The signed order/trade data
  status       String   @default("pending") // pending | submitted | confirmed | failed
  txHash       String?
  retryCount   Int      @default(0)
  errorMessage String?
  userId       String
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
}

// Transaction states
// pending    -> waiting to be submitted
// submitted  -> TX sent, waiting for confirmation
// confirmed  -> TX confirmed on-chain
// failed     -> TX failed after max retries

Priority 2: Batch Order Matching with Multicall

Use the same pattern as handleBatchCancel() for order matching.

// Server-side batch matching (api/src/routes/trade.ts)
router.post('/execute-batch', async (req, res) => {
  const { orderPairs } = req.body; // Array of {takerOrder, makerOrder}

  // Encode all matchOrders calls
  const calls = orderPairs.map(({ taker, maker }) =>
    ctfExchange.interface.encodeFunctionData(
      "matchOrders",
      [taker, [maker], taker.takerAmount, maker.makerAmount, 0]
    )
  );

  // Single atomic transaction
  const tx = await ctfExchange.multicall(calls);
  const receipt = await tx.wait();

  // Save all trades to DB after confirmation
  await prisma.trade.createMany({
    data: orderPairs.map(pair => ({
      // ... trade details
      txHash: receipt.transactionHash,
      status: 'confirmed'
    }))
  });

  return res.json({ success: true, txHash: receipt.transactionHash });
});

Priority 3: Single Signature for Sweep Operations

User signs once for entire trade amount, server splits as needed.

// Frontend: useTrade.ts
const executeSweep = async (side: 'BUY' | 'SELL', totalAmount: bigint) => {
  // Sign a single "intent" order for total amount
  const sweepIntent = {
    maker: userAddress,
    side: side,
    tokenId: outcomeTokenId,
    makerAmount: side === 'BUY' ? totalUSDCNeeded : totalTokensToSell,
    takerAmount: side === 'BUY' ? totalTokensExpected : totalUSDCExpected,
    nonce: Date.now(),
    expiration: Math.floor(Date.now() / 1000) + 3600,
  };

  // Single signature
  const signature = await signer.signTypedData(domain, ORDER_TYPES, sweepIntent);

  // Server handles splitting into sub-orders for matching
  const response = await fetch(`${API_URL}/api/trade/sweep`, {
    method: 'POST',
    body: JSON.stringify({
      intent: sweepIntent,
      signature,
      maxSlippage: 0.01 // 1% slippage tolerance
    })
  });

  return response.json();
};

Priority 4: Background Transaction Processor

A worker that processes the transaction queue with retry logic.

// api/src/workers/transactionProcessor.ts
class TransactionProcessor {
  private readonly MAX_RETRIES = 3;
  private readonly RETRY_DELAY_MS = 5000;

  async processQueue() {
    // Get pending transactions
    const pending = await prisma.transactionQueue.findMany({
      where: { status: 'pending' },
      orderBy: { createdAt: 'asc' },
      take: 10
    });

    for (const tx of pending) {
      try {
        // Update to submitted
        await prisma.transactionQueue.update({
          where: { id: tx.id },
          data: { status: 'submitted' }
        });

        // Execute based on type
        const result = await this.executeTransaction(tx);

        // Update to confirmed
        await prisma.transactionQueue.update({
          where: { id: tx.id },
          data: {
            status: 'confirmed',
            txHash: result.transactionHash
          }
        });

      } catch (error) {
        await this.handleError(tx, error);
      }
    }
  }

  private async handleError(tx: TransactionQueue, error: Error) {
    const newRetryCount = tx.retryCount + 1;

    if (newRetryCount >= this.MAX_RETRIES) {
      await prisma.transactionQueue.update({
        where: { id: tx.id },
        data: {
          status: 'failed',
          errorMessage: error.message,
          retryCount: newRetryCount
        }
      });
      // Notify user of failure
      await this.notifyUser(tx.userId, 'Transaction failed after retries');
    } else {
      // Reset to pending for retry
      await prisma.transactionQueue.update({
        where: { id: tx.id },
        data: {
          status: 'pending',
          retryCount: newRetryCount,
          errorMessage: error.message
        }
      });
    }
  }
}

5. Suggested Implementation Order

6. Files to Modify

File Changes
prisma/schema.prisma Add TransactionQueue model
api/src/routes/trade.ts Add /execute-batch endpoint with multicall
api/src/routes/orders.ts Update to save after TX confirmation
api/src/workers/transactionProcessor.ts New file - background queue processor
web/src/hooks/useTrade.ts Update executeTrade() for batch API