Task-1 Review: Order/Trade Batch Processing Architecture
December 9, 2025
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.
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.
orders.ts - order creation
Orders saved to DB before blockchain confirmation. Can cause DB/blockchain state mismatch if TX fails after DB write.
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.
Updated OrderSeedService to use integer-based directional rounding.
• Sells (Asks): Round Shares UP (Ceil) → Price slightly LOWER
(Taker
friendly).
• Buys (Bids): Round Shares DOWN (Floor) → Price slightly HIGHER
(Maker
friendly).
Backend matchOrders logic now performs explicit BigInt cross-multiplication
(UserPrice vs MakerPrice) to verify mathematical crossing before
submitting the transaction.
Instead of attempting only the top order, the matcher now iterates through the order book, skipping incompatible "dust" orders and executing against valid liquidity without crashing.
| 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 |
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
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 });
});
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();
};
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
}
});
}
}
}
Prisma schema update + migration. Foundation for all other changes.
Server-side multicall for batch order matching. Reuse pattern from handleBatchCancel.
Modify executeTrade() to collect all orders, then single API call.
Background job for retry logic. Can use node-cron or similar.
Single signature for entire sweep. More complex but best UX.
| 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 |