Order polling is the core mechanism by which the Watch Tower checks conditional orders to determine if they should be posted to the CoW Protocol OrderBook. This process runs continuously for every active conditional order on each new block.
Polling overview
The Watch Tower polls conditional orders by calling their handler contracts to check if the order conditions are met. When conditions are satisfied, the handler returns a discrete order that can be submitted to the OrderBook.
Polling happens on every block for active orders, unless the previous poll result indicates the order should be checked at a specific future time or block.
How polling works
The polling process involves several steps:
1. Order iteration
For each block, the Watch Tower iterates through all registered owners and their conditional orders:
for (const [owner, conditionalOrders] of ownerOrders.entries()) {
for (const conditionalOrder of conditionalOrders) {
// Poll each conditional order
}
}
2. Poll scheduling
Before polling, the Watch Tower checks if the order is due to be checked based on previous poll results:
Skip until epoch:
if (
lastHint?.result === PollResultCode.TRY_AT_EPOCH &&
blockTimestamp < lastHint.epoch
) {
// Skip this order until the specified timestamp
continue;
}
Skip until block:
if (
lastHint?.result === PollResultCode.TRY_ON_BLOCK &&
blockNumber < lastHint.blockNumber
) {
// Skip this order until the specified block
continue;
}
3. Handler invocation
The Watch Tower calls the conditional order handler to check if conditions are met:
export async function pollConditionalOrder(
conditionalOrderId: string,
pollParams: PollParams,
conditionalOrderParams: ConditionalOrderParams,
chainId: SupportedChainId,
blockNumber: number,
ownerNumber: number,
orderNumber: number
): Promise<PollResult | undefined> {
const order = ordersFactory.fromParams(conditionalOrderParams);
if (!order) {
return undefined;
}
return order.poll(actualPollParams);
}
4. Result handling
The poll result indicates what action to take:
enum PollResultCode {
SUCCESS, // Order is ready to be posted
TRY_AT_EPOCH, // Check again at specific timestamp
TRY_ON_BLOCK, // Check again at specific block
TRY_NEXT_BLOCK, // Check again on next block
DONT_TRY_AGAIN, // Stop watching this order
UNEXPECTED_ERROR // An error occurred
}
Poll parameters
When polling an order, the Watch Tower provides comprehensive context:
interface PollParams {
owner: string; // Order owner address
chainId: SupportedChainId;
proof: string[]; // Merkle proof if applicable
offchainInput: string; // Additional off-chain data
blockInfo: {
blockTimestamp: number;
blockNumber: number;
};
provider: Provider; // Ethereum provider
orderBookApi: OrderBookApi;
}
The Watch Tower uses the processing block’s timestamp and number, not the latest block, to ensure consistent indexing.
Poll results and discrete orders
When a poll succeeds, it returns a discrete order ready for submission:
interface PollResultSuccess {
result: PollResultCode.SUCCESS;
order: GPv2Order.DataStruct; // The actual order to submit
signature: string; // EIP-1271 signature
}
Order validation
Before submitting, the Watch Tower validates the order:
try {
badOrder.check(orderToSubmit);
} catch (e: any) {
return {
result: PollResultCode.DONT_TRY_AGAIN,
reason: `Invalid order: ${e.message}`,
};
}
Validation checks include:
- Valid token addresses
- Non-zero amounts
- Reasonable expiration times
- Supported order types
Order UID calculation
Each discrete order gets a unique identifier:
const orderUid = computeOrderUid(
{
name: "Gnosis Protocol",
version: "v2",
chainId: chainId,
verifyingContract: GPV2SETTLEMENT,
},
orderToSubmit,
owner
);
Submitting to the OrderBook
When an order is ready, it’s posted to the CoW Protocol OrderBook API:
if (!conditionalOrder.orders.has(orderUid)) {
const orderUid = await orderBookApi.sendOrder(postOrder);
conditionalOrder.orders.set(orderUid, OrderStatus.SUBMITTED);
}
Duplicate detection
The Watch Tower tracks submitted orders to avoid duplicates:
if (conditionalOrder.orders.has(orderUid)) {
const orderStatus = conditionalOrder.orders.get(orderUid);
log.info(`OrderUid ${orderUid} status: ${formatStatus(orderStatus)}`);
// Skip resubmission
}
Integration with the SDK
The Watch Tower uses the CoW Protocol SDK for polling:
import {
ConditionalOrderFactory,
ConditionalOrderParams,
DEFAULT_CONDITIONAL_ORDER_REGISTRY,
PollParams,
PollResult,
} from "@cowprotocol/sdk-composable";
const ordersFactory = new ConditionalOrderFactory(
DEFAULT_CONDITIONAL_ORDER_REGISTRY
);
Handler registry
The SDK maintains a registry of known order handlers:
- TWAP: Time-weighted average price orders
- Limit orders: Orders at specific price points
- Stop-loss: Orders that trigger at price thresholds
- Custom handlers: User-defined order logic
If a handler is not recognized by the SDK, the Watch Tower falls back to legacy polling using direct contract calls.
Legacy polling
For unsupported order types, the Watch Tower uses direct contract calls:
const callData = contract.interface.encodeFunctionData(
"getTradeableOrderWithSignature",
[owner, conditionalOrder.params, offchainInput, proof]
);
const lowLevelCall = await multicall.callStatic.aggregate3Value([{
target: conditionalOrder.composableCow,
allowFailure: true,
value: 0,
callData,
}]);
This approach:
- Uses multicall for gas efficiency
- Handles contract reverts gracefully
- Extracts custom error hints from the handler
Error handling
The Watch Tower handles various error scenarios during polling:
API errors
When submitting to the OrderBook fails:
Temporary errors (retry next block):
- Quote not found
- Invalid quote
- Invalid EIP-1271 signature
Backoff errors (retry after delay):
- Insufficient allowance (retry in 10 minutes)
- Insufficient balance (retry in 10 minutes)
- Too many limit orders (retry in 1 hour)
Permanent errors (don’t try again):
- Zero amount
- Unsupported tokens
- Invalid order parameters
On-chain errors
Contract reverts may include hints:
return handleOnChainCustomError({
owner,
chainId,
target,
callData,
revertData: returnData,
metricLabels,
blockNumber,
ownerNumber,
orderNumber,
});
Common hints:
PollTryAtEpoch(uint256 epoch): Try again at specific time
PollTryAtBlock(uint256 blockNumber): Try again at specific block
PollNever(string reason): Stop trying
Batched persistence
The Watch Tower saves the registry periodically during polling:
const CHUNK_SIZE = 50;
if (updatedCount % CHUNK_SIZE === 1 && updatedCount > 1) {
await registry.write();
}
This reduces disk I/O while ensuring data isn’t lost.
Filter policies
Orders can be filtered before polling to reduce load:
if (filterPolicy) {
const filterResult = filterPolicy.preFilter({
conditionalOrderId,
transaction: conditionalOrder.tx,
owner,
conditionalOrderParams: conditionalOrder.params,
});
switch (filterResult) {
case policy.FilterAction.DROP:
// Permanently remove
ordersPendingDelete.push(conditionalOrder);
continue;
case policy.FilterAction.SKIP:
// Skip this block only
continue;
}
}
Filter policies allow operators to customize which orders are watched, useful for managing RPC costs or focusing on specific order types.
Metrics and monitoring
The Watch Tower tracks detailed metrics for polling:
pollingRunsTotal: Total number of poll attempts
pollingOnChainDurationSeconds: Time spent in on-chain calls
pollingOnChainChecksTotal: Count of on-chain checks
pollingUnexpectedErrorsTotal: Count of unexpected errors
orderBookDiscreteOrdersTotal: Orders successfully posted
orderBookErrorsTotal: API submission errors by type