Trading Engine
SmartRouter, exchange adapters, custody models, and order state machine
The trading engine handles order submission, routing, execution, and lifecycle management. It routes orders to the correct exchange, manages state transitions, and supports advanced order types including TP/SL, sniper orders, and counter-trade automation.
Vezta integrates two exchanges with very different custody and signing models:
| Exchange | Custody | Signer | Settlement chain |
|---|---|---|---|
| Polymarket | Per-user Gnosis Safe (1-of-1) on Polygon | Backend-derived EOA (encrypted) | Polygon (USDC.e) |
| Kalshi (via DFlow) | User's own Solana wallet | User's wallet (Phantom/Solflare) | Solana (USDC) |
Order Submission Flow
SmartRouter
SmartRouterService selects the adapter based on the market's source. Before invoking an adapter, it:
- Computes a deterministic SHA-256 idempotency key (
userId:marketId:side:amount:5minBucket) and skips if seen in Redis (5-min TTL) - Checks the per-exchange circuit breaker (
circuit-breaker:{exchange}Redis key, 5-min auto-reset TTL) - Validates pending balance: subtracts totals of
SUBMITTING/OPEN/PARTIALLY_FILLEDorders before approving the new one - For DFlow (Kalshi): the DFlow adapter checks the maintenance window (Thursdays 3-5 AM ET) and rejects with
KALSHI_MAINTENANCEerror during it
For markets that exist on both Polymarket and Kalshi, the router considers price and liquidity to find the best execution venue.
Exchange Adapters
Adapters live in src/modules/trading/adapters/ and implement a common interface.
Polymarket Adapter
- Submits orders via the Polymarket CLOB API
- CLOB API credentials are auto-derived from the user's wallet signature (no manual input needed)
- The user's EOA signs the order; the Safe holds USDC.e and settles
- Safe deployment + USDC approvals are gasless via the Builder Relayer
- Supports market orders, limit orders, FAK, FOK, GTC, Post Only
DFlow Adapter
- Submits orders via the DFlow Trade API proxied through the backend (
/api/v1/dflow/*) due to CORS - Returns an unsigned
VersionedTransaction(base64) for the frontend to sign with the user's Solana wallet - After the user signs and the frontend submits to Solana RPC, the backend polls
GET /api/v1/dflow/order-status?signature={sig}until status =closed - Positions are SPL Token-2022 outcome tokens in the user's Solana wallet
- Market orders only (limit orders not yet supported by DFlow)
Adapter Retry
Both adapters retry failed orders 3x with exponential backoff (100ms base). 4xx errors are not retried.
Order State Machine
OrderStateMachine enforces valid state transitions. The Prisma OrderStatus enum values used:
| State | Description |
|---|---|
CREATED | Order validated and saved |
SUBMITTING | Submitted to exchange, awaiting confirmation |
OPEN | Resting on the order book (limit only) |
PARTIALLY_FILLED | Some quantity executed |
FILLED | Fully executed (terminal) |
CANCELLING / CANCELLED | In-progress / cancelled (terminal) |
REJECTED | Exchange rejected the order |
FAILED | Submission or execution failed (terminal) |
The state machine maps its own internal string states to the Prisma enum. Once an order reaches a terminal state (FILLED, CANCELLED, REJECTED, FAILED), no further transitions are allowed.
Order Reconciliation
reconcileStaleOrders() runs periodically and catches orders stuck in SUBMITTING for >2 minutes:
- If no external order ID exists, marks as
FAILED - If an external order ID exists, logs the mismatch for manual review
Take-Profit and Stop-Loss (TP/SL)
Positions can have optional takeProfitPrice and stopLossPrice fields. The price-sync cycle monitors all open positions and submits market sell orders when the trigger condition fires:
- Take-Profit: Sell when
currentPrice >= takeProfitPrice(long YES) /<= takeProfitPrice(long NO) - Stop-Loss: Sell when
currentPrice <= stopLossPrice(long YES) />= stopLossPrice(long NO)
Sniper Orders
Sniper orders are price-triggered buy orders managed by SniperMonitorScheduler:
| Field | Type | Description |
|---|---|---|
triggerPrice | Decimal(10,6) | Price threshold that triggers execution |
amount | Decimal(18,6) | Order size in USD |
slippage | Decimal(5,4) | Maximum acceptable slippage (default 2%) |
status | SniperStatus | WATCHING, EXECUTING, FILLED, EXPIRED, CANCELLED, INSUFFICIENT_BALANCE, FAILED |
expiresAt | DateTime | Expiration timestamp |
repeat | Boolean | Whether to re-arm after filling |
Counter-Trade Automation
CounterTradeConfig allows users to automatically trade inversely against a target wallet:
| Field | Type | Description |
|---|---|---|
targetWalletAddress | String | Wallet to counter-trade against |
strategy | String | inverse (default) or mirror |
sizeMultiplier | Decimal(5,2) | Multiplier applied to the target's trade size |
executionDelaySec | Int | Optional delay before executing |
maxTradeSize | Decimal? | Per-trade size cap |
dailyStopLoss | Decimal? | Daily loss limit before pausing |
categories | String[] | Market categories to counter-trade in |
CounterTradeResetScheduler resets daily PnL and trade counts at midnight UTC.
Counter-trade only works on Polymarket (Kalshi-via-DFlow trades are anonymized at the on-chain layer).
Cross-Chain Withdrawals
Withdrawals from the Polygon Safe to other chains use LI.FI (li.quest/v1/quote):
- Native chains (Polygon, Solana) -- direct via Safe relayer / Solana keypair
- Other EVM chains (Ethereum, Arbitrum, Optimism, Base, Avalanche, BNB, Linea, zkSync, Scroll) -- single relayed Safe execution with
[approve(USDC, lifiDiamond), call(lifiDiamond, bridgeData)]
Allowlist lives in WITHDRAW_CHAINS (vezta-be/src/modules/account/dto/withdraw.dto.ts); must stay in sync with USDC_ADDRESSES / LIFI_CHAIN_KEYS in lifi-bridge.service.ts and the frontend CHAINS array in withdraw-modal.tsx.
Bridge withdrawals always source from the Polygon Safe balance, not the destination chain's balance. Make sure the user has enough USDC.e in the Safe before initiating a withdrawal.
Order Fields Reference
Key fields on the Order model:
| Field | Type | Description |
|---|---|---|
side | String | yes or no |
type | String | market or limit |
action | String | buy or sell |
price | Decimal(10,6) | Order price |
quantity | Decimal(18,6) | Requested quantity |
filledQuantity | Decimal(18,6) | Quantity filled so far |
avgFillPrice | Decimal(10,6)? | Average execution price |
fee | Decimal(18,6) | Trading fee |
exchange | String | polymarket or dflow |
source | String | manual, copy-trade, sniper, counter-trade |
copyTradeSubId | String? | Link to copy trade subscription if applicable |
Combo Trades
Beyond single-market orders, Vezta supports combos — multi-leg (parlay-style) Polymarket CLOB orders where several outcomes are bundled into one position that pays out only if every leg resolves in the buyer's favour. This path is handled by the combo/ module rather than the SmartRouter:
- Eligibility catalog —
ComboCatalogController(/api/v1/combos) exposes which markets can be combined per game (ComboEligibleMarket); the catalog sync runs on themarketdomain. - Placement —
ComboController(/api/v1/combo) accepts a combo order asynchronously (returns202 Accepted). Legs are EIP-712 signed (combo-signing.service.ts) and submitted through the Polymarket combo / RFQ gateway. - Persistence — a
ComboOrderwith itsComboLegrows tracks status; updates are pushed on theuser:combo-ordersWebSocket channel. - Settlement — the
combosworker domain (worker-combos) runs settlement reconciliation (combo-settle.processor.ts); anexitcloses an open combo early via a SELL RFQ andredeemclaims a settled winning combo.
See the Combos API for the full endpoint list.