Skip to main content
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

Performance optimization

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

Build docs developers (and LLMs) love