---
id: TIP-1003
title: Client order IDs
description: Addition of client order IDs to the Stablecoin DEX, allowing users to specify their own order identifiers for idempotency and easier order tracking.
authors: Dan Robinson
status: Draft
---

# TIP-1003: Client order IDs

## Abstract

This TIP adds support for optional client order IDs (`clientOrderId`) to the Stablecoin DEX. Users can specify a `uint128` identifier when placing orders, which serves as an idempotency key and a predictable handle for the order. The system-generated `orderId` is not predictable before transaction execution, making client order IDs useful for order management.

## Motivation

Traditional exchanges allow users to specify a client order ID (called `ClOrdID` in FIX protocol, `cloid` in Hyperliquid) for several reasons:

1. **Idempotency**: If a transaction is submitted twice (e.g., due to network issues), the duplicate can be detected and rejected
2. **Predictable reference**: Users know the order identifier before the transaction confirms, enabling them to prepare cancel requests or track orders without waiting for confirmation
3. **Integration**: External systems can use their own ID schemes to correlate orders

---

# Specification

## New storage

A new mapping tracks active client order IDs per user:

```solidity
mapping(address user => mapping(uint128 clientOrderId => uint128 orderId)) public clientOrderIds;
```

## Modified functions

All order placement functions gain an optional `clientOrderId` parameter:

```solidity
/// @notice Places an order with an optional client order ID
/// @param token The base token of the pair
/// @param amount The order amount in base tokens
/// @param isBid True for buy orders, false for sell orders
/// @param tick The price tick for the order
/// @param clientOrderId Optional client-specified ID (0 for none)
/// @return orderId The system-assigned order ID
function place(
    address token,
    uint128 amount,
    bool isBid,
    int16 tick,
    uint128 clientOrderId
) external returns (uint128 orderId);

/// @notice Places an order on a specific pair with an optional client order ID
/// @dev Overload from TIP-1001
function place(
    bytes32 bookKey,
    address token,
    uint128 amount,
    bool isBid,
    int16 tick,
    uint128 clientOrderId
) external returns (uint128 orderId);

/// @notice Places a flip order with an optional client order ID
function placeFlip(
    address token,
    uint128 amount,
    bool isBid,
    int16 tick,
    int16 flipTick,
    bool internalBalanceOnly,
    uint128 clientOrderId
) external returns (uint128 orderId);

/// @notice Places a flip order on a specific pair with an optional client order ID
/// @dev Overload from TIP-1001
function placeFlip(
    bytes32 bookKey,
    address token,
    uint128 amount,
    bool isBid,
    int16 tick,
    int16 flipTick,
    bool internalBalanceOnly,
    uint128 clientOrderId
) external returns (uint128 orderId);
```

## New functions

```solidity
/// @notice Cancels an order by its client order ID
/// @param clientOrderId The client-specified order ID
function cancelByClientOrderId(uint128 clientOrderId) external;

/// @notice Gets the system order ID for a client order ID
/// @param user The user who placed the order
/// @param clientOrderId The client-specified order ID
/// @return orderId The system-assigned order ID, or 0 if not found
function getOrderByClientOrderId(address user, uint128 clientOrderId) external view returns (uint128 orderId);
```

## Behavior

### Placing orders with clientOrderId

When `clientOrderId` is non-zero:

1. Check if `clientOrderIds[msg.sender][clientOrderId]` maps to an active order
2. If it does, revert with `DUPLICATE_CLIENT_ORDER_ID`
3. Otherwise, proceed with order placement and set `clientOrderIds[msg.sender][clientOrderId] = orderId`

When `clientOrderId` is zero, no client order ID tracking occurs.

### Uniqueness and reuse

A `clientOrderId` must be unique among a user's **active orders**. Once an order is filled or cancelled, its `clientOrderId` can be reused. This matches the standard FIX protocol behavior where `ClOrdID` uniqueness is required only for working orders.

When an order reaches a terminal state (filled or cancelled), the `clientOrderIds` mapping entry is cleared.

### Flip orders

When a flip order is filled and creates a new order on the opposite side:

1. The new (flipped) order inherits the original order's `clientOrderId`
2. The `clientOrderIds` mapping is updated to point to the new order ID
3. This allows users to track their position across flips using a single `clientOrderId`

If the original order had no `clientOrderId` (was zero), the flipped order also has no `clientOrderId`.

### Cancellation

`cancelByClientOrderId(clientOrderId)` looks up `clientOrderIds[msg.sender][clientOrderId]` and cancels that order. It reverts if no active order exists for that `clientOrderId`.

## New event

```solidity
/// @notice Emitted when an order is placed (V2 with clientOrderId)
/// @dev Replaces OrderPlaced for new orders
event OrderPlacedV2(
    uint128 indexed orderId,
    address indexed maker,
    address token,
    uint128 amount,
    bool isBid,
    int16 tick,
    bool isFlipOrder,
    int16 flipTick,
    uint128 clientOrderId
);
```

`OrderPlacedV2` is identical to `OrderPlaced` but adds the `clientOrderId` field. When an order is placed, only `OrderPlacedV2` is emitted (not both events).

## New errors

```solidity
/// @notice The client order ID is already in use by an active order
error DUPLICATE_CLIENT_ORDER_ID();

/// @notice No active order found for the given client order ID
error CLIENT_ORDER_ID_NOT_FOUND();
```

---

# Invariants

- A non-zero `clientOrderId` maps to at most one active order per user
- `clientOrderIds[user][clientOrderId]` is cleared when the order is filled or cancelled
- Flip orders inherit `clientOrderId` and update the mapping atomically
- `clientOrderId = 0` is reserved to mean "no client order ID"
