> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chainstack.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Handling order precision and market metadata

> Query Hyperliquid metadata to dynamically calculate valid order sizes, tick precision, and lot sizes across different trading assets using the SDK.

Hyperliquid rejects orders with wrong precision. BTC requires 5 decimal places for size, PURR needs 0 (whole integers only), and others vary from 0 to 8. If you hardcode precision, your bot breaks when trading new assets. This guide shows you how to query metadata and calculate valid order sizes dynamically.

<Info>
  **Prerequisites**

  * Python 3.8 or higher
  * `hyperliquid-python-sdk` installed (`pip install hyperliquid-python-sdk`)
  * [Reliable Hyperliquid RPC endpoint](https://chainstack.com/build-better-with-hyperliquid/) ([sign up for free](https://console.chainstack.com/))
</Info>

## The precision problem

This order gets rejected:

```python theme={"system"}
# Wrong: BTC spot order with 3 decimals
exchange.order(
    name="@0",  # BTC spot
    is_buy=True,
    sz=0.001,   # Rejected: BTC spot needs exactly 5 decimals
    limit_px=65000.0,
    order_type={"limit": {"tif": "Gtc"}}
)
```

Error: `"Tick size violation"` or `"Invalid order size"`

This works:

```python theme={"system"}
# Correct: BTC spot with 5 decimals
exchange.order(
    name="@0",
    is_buy=True,
    sz=0.00100,  # Valid: 5 decimal places
    limit_px=65000.0,
    order_type={"limit": {"tif": "Gtc"}}
)
```

Hardcoding `0.00100` breaks when you switch to another asset. You need dynamic precision.

## Tick size vs lot size

Hyperliquid enforces precision on both price and quantity:

**Tick size** — price precision:

* Prices limited to **5 significant figures**
* Max decimal places: `MAX_DECIMALS - szDecimals`, where `MAX_DECIMALS` is 6 for perps and 8 for spot
* Integer prices are always valid regardless of significant figures (for example, `123456` is valid for BTC)
* Examples for BTC perp (`szDecimals` = 5, so max 1 decimal place): `97000.5` ✓, `97000.55` ✗

**Lot size** — order quantity precision:

* Determined by `szDecimals` in asset metadata, fixed per asset at deployment
* Ranges from 0 (whole integers only, for example, PURR) to 5 (for example, BTC)
* Must query from API—cannot hardcode

Both are enforced at the API level. Violating either causes immediate order rejection.

## Querying asset metadata

### Understanding Hyperliquid asset formats

Hyperliquid uses multiple formats to identify assets:

**Spot assets**:

* `@{index}` — direct index reference (for example, `@0` = BTC/USDC)
* `{TOKEN}/USDC` — pair name (for example, `PURR/USDC`)

**Perpetual assets**:

* `{SYMBOL}` — direct symbol (for example, `BTC`, `ETH`, `SOL`)

You need to handle all three formats when querying metadata.

### Getting spot asset metadata

Query [`spotMetaAndAssetCtxs`](/reference/hyperliquid-info-spot-meta-and-asset-ctxs) for comprehensive spot data including precision requirements:

```python theme={"system"}
from hyperliquid.info import Info

def get_spot_asset_info(info: Info, coin_field: str) -> dict:
    """Get price and size decimals for spot assets"""

    # Get both metadata and live contexts
    spot_data = info.spot_meta_and_asset_ctxs()
    spot_meta = spot_data[0]   # Metadata (universe, tokens)
    asset_ctxs = spot_data[1]  # Live data (prices, volumes)

    if coin_field.startswith("@"):
        # Handle @index format
        index = int(coin_field[1:])

        if index >= len(asset_ctxs):
            raise ValueError(f"Invalid spot index: {coin_field}")

        # Get current price (try midPx first, fallback to markPx)
        ctx = asset_ctxs[index]
        price = float(ctx.get("midPx", ctx.get("markPx", 0)))

        if price <= 0:
            raise ValueError(f"No price available for {coin_field}")

        # Find pair metadata
        pair_info = next(
            (p for p in spot_meta["universe"] if p["index"] == index),
            None
        )

        if not pair_info:
            raise ValueError(f"No metadata for {coin_field}")

        # Get base token size decimals
        token_indices = pair_info["tokens"]
        base_token_index = token_indices[0]
        token_info = spot_meta["tokens"][base_token_index]
        size_decimals = token_info.get("szDecimals", 6)

        return {
            "coin": coin_field,
            "price": price,
            "szDecimals": size_decimals,
            "name": pair_info.get("name", coin_field)
        }

    elif "/" in coin_field:
        # Handle PAIR/USDC format - convert to @index
        pair = next(
            (p for p in spot_meta["universe"] if p["name"] == coin_field),
            None
        )

        if not pair:
            raise ValueError(f"Pair not found: {coin_field}")

        # Get info using @index format
        return get_spot_asset_info(info, f"@{pair['index']}")

    else:
        raise ValueError(f"Unsupported spot format: {coin_field}")
```

### Getting perpetual asset metadata

Perps use a simpler structure via [`metaAndAssetCtxs`](/reference/hyperliquid-info-meta-and-asset-ctxs):

```python theme={"system"}
def get_perp_asset_info(info: Info, symbol: str) -> dict:
    """Get price and size decimals for perpetual contracts"""

    meta_and_contexts = info.meta_and_asset_ctxs()
    meta = meta_and_contexts[0]
    asset_ctxs = meta_and_contexts[1]

    # Find asset in universe
    asset_index = None
    for i, asset in enumerate(meta["universe"]):
        if asset["name"] == symbol:
            asset_index = i
            break

    if asset_index is None:
        raise ValueError(f"Perp asset not found: {symbol}")

    # Get metadata
    asset_meta = meta["universe"][asset_index]
    size_decimals = asset_meta.get("szDecimals", 8)

    # Get current price
    ctx = asset_ctxs[asset_index]
    price = float(ctx.get("markPx", "0"))

    if price <= 0:
        raise ValueError(f"No price for {symbol}")

    return {
        "coin": symbol,
        "price": price,
        "szDecimals": size_decimals,
        "name": asset_meta["name"]
    }
```

## Working with order precision

### Calculating valid order sizes (lot size)

Order sizes must be rounded to the `szDecimals` specified in asset metadata. This is also known as the lot size—the minimum increment for order quantities.

```python theme={"system"}
import math

def calculate_order_size(target_value_usdc: float, asset_info: dict) -> float:
    """Calculate order size with correct precision (lot size)"""

    price = asset_info["price"]
    size_decimals = asset_info["szDecimals"]

    # Calculate raw size
    raw_size = target_value_usdc / price

    # Floor-truncate to valid precision (never round up beyond balance)
    valid_size = math.floor(raw_size * 10**size_decimals) / (10**size_decimals)

    return valid_size
```

<Warning>
  Use `math.floor`, not `round`, for size truncation. Rounding up can produce a size larger than your balance, causing order rejection. This matches the [official SDK rounding example](https://github.com/hyperliquid-dex/hyperliquid-python-sdk/blob/master/examples/rounding.py).
</Warning>

**Example** (from the copy trading implementation):

* Target value: \$15 USDC
* BTC spot price: \$65,000
* Size decimals (`szDecimals`): 5
* Raw size: 15 / 65000 = 0.000230769...
* Valid size: `floor(0.000230769 * 100000) / 100000` = `0.00023` (5 decimals)

The `szDecimals` field in metadata determines lot size precision. Query this value from `spot_meta_and_asset_ctxs()` for spot or `meta_and_asset_ctxs()` for perps.

### Minimum notional value

Hyperliquid enforces [minimum order values](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/orders) of \$10 (or 10 USDC for spot). The only exception is exactly closing a position with a reduce-only order. Check before placing:

```python theme={"system"}
def validate_order_size(size: float, price: float, min_notional: float = 10.0) -> bool:
    """Verify order meets minimum notional value"""

    notional_value = size * price

    if notional_value < min_notional:
        raise ValueError(
            f"Order value \${notional_value:.2f} below minimum \${min_notional}"
        )

    return True
```

### Price precision (tick size)

Price precision on Hyperliquid follows [specific rules](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size):

**Significant figures rule** — prices can have up to **5 significant figures**

**Integer exception** — integer prices are always valid regardless of significant figures (for example, `123456` is valid for BTC even though it has 6 significant figures)

**Maximum decimal places** — determined by the formula `MAX_DECIMALS - szDecimals`:

* **Perpetuals** — `MAX_DECIMALS` = 6
* **Spot** — `MAX_DECIMALS` = 8

This means the allowed price decimal places depend on the asset. Examples for perpetuals:

| Asset | `szDecimals` | Max price decimals (`6 - szDecimals`) | Example valid price |
| ----- | ------------ | ------------------------------------- | ------------------- |
| BTC   | 5            | 1                                     | `97000.5`           |
| ETH   | 4            | 2                                     | `2567.35`           |
| BNB   | 3            | 3                                     | `625.123`           |
| ATOM  | 2            | 4                                     | `9.1234`            |
| DYDX  | 1            | 5                                     | `1.01234`           |
| TRX   | 0            | 6                                     | `0.123456`          |

**Examples of valid and invalid perpetual prices** (BTC, `szDecimals` = 5):

* `97000.5` ✓ (5 sig figs, 1 decimal)
* `97000.55` ✗ (exceeds `6 - 5 = 1` max decimal places)
* `123456` ✓ (integer exception—always valid)

**Examples of valid and invalid perpetual prices** (general):

* `1234.5` ✓ (5 significant figures)
* `1234.56` ✗ (6 significant figures—too many)
* `0.001234` ✓ (4 significant figures, 6 decimals)

**Price formatting for API**:

```python theme={"system"}
def format_price(price: float, sz_decimals: int, is_spot: bool = False) -> float:
    """Format price matching the official SDK rounding logic.

    Uses 5 significant figures with MAX_DECIMALS - szDecimals decimal places.
    Integer prices above 100,000 are always valid.
    """

    max_decimals = 8 if is_spot else 6

    if price > 100_000:
        # Integer prices always valid regardless of sig figs
        return round(price)

    # 5 significant figures, then cap decimal places
    return round(float(f"{price:.5g}"), max_decimals - sz_decimals)

# Examples
format_price(97123.456, sz_decimals=5)              # 97123.0 (BTC perp, max 1 decimal)
format_price(2567.891, sz_decimals=4)                # 2567.9 (ETH perp, max 2 decimals)
format_price(0.12345678, sz_decimals=2, is_spot=True) # 0.12346 (spot, max 6 decimals)
```

<Warning>
  When signing orders, trailing zeros in price and size strings cause the signature hash to change, producing the misleading error `"User or API Wallet 0x1a2B3c4D5e6F7a8B9c0D1e2F3a4B5c6D7e8F9a0b does not exist."` The SDK handles this automatically via `float_to_wire()`, which uses Python's `Decimal.normalize()` to strip trailing zeros. If you build order wire messages manually, strip trailing zeros before signing.
</Warning>

## Implementation examples

### Complete order placement flow

Putting it together for dynamic multi-asset trading:

```python theme={"system"}
def place_dynamic_order(
    exchange: Exchange,
    info: Info,
    coin_field: str,
    is_buy: bool,
    target_value_usdc: float,
    price_offset_pct: float = 0
):
    """Place order with automatic precision handling"""

    # Determine asset type
    is_spot = coin_field.startswith("@") or "/" in coin_field

    # Get asset info
    if is_spot:
        asset_info = get_spot_asset_info(info, coin_field)
    else:
        asset_info = get_perp_asset_info(info, coin_field)

    # Calculate order size with proper precision
    order_size = calculate_order_size(target_value_usdc, asset_info)

    # Validate minimum notional
    validate_order_size(order_size, asset_info["price"])

    # Calculate limit price with offset
    market_price = asset_info["price"]
    offset_multiplier = 1 + (price_offset_pct / 100)
    raw_price = market_price * offset_multiplier

    # Round price to tick size using the correct formula
    limit_price = format_price(raw_price, asset_info["szDecimals"], is_spot)

    print(f"Placing order: {order_size} {coin_field} @ \${limit_price}")

    # Place order
    result = exchange.order(
        name=coin_field,
        is_buy=is_buy,
        sz=order_size,
        limit_px=limit_price,
        order_type={"limit": {"tif": "Gtc"}},
        reduce_only=False
    )

    return result
```

### Handling unknown assets

When trading new or obscure assets:

```python theme={"system"}
from typing import Optional

def get_asset_info_safe(info: Info, coin_field: str) -> Optional[dict]:
    """Get asset info with graceful fallback"""

    try:
        if coin_field.startswith("@") or "/" in coin_field:
            return get_spot_asset_info(info, coin_field)
        else:
            return get_perp_asset_info(info, coin_field)
    except Exception as e:
        print(f"Warning: failed to get metadata for {coin_field}: {e}")

        # Fallback to safe defaults
        return {
            "coin": coin_field,
            "price": None,
            "szDecimals": 8,  # Conservative default (most restrictive)
            "name": coin_field
        }
```

Use conservative defaults when metadata is unavailable, but always warn users. A higher `szDecimals` default is safer because it produces a more precisely truncated (smaller) size via `math.floor`.

### Caching metadata

Query metadata once, reuse for multiple orders:

```python theme={"system"}
class AssetMetadataCache:
    def __init__(self, info: Info):
        self.info = info
        self.cache = {}

    def get_asset_info(self, coin_field: str) -> dict:
        if coin_field not in self.cache:
            if coin_field.startswith("@") or "/" in coin_field:
                self.cache[coin_field] = get_spot_asset_info(
                    self.info, coin_field
                )
            else:
                self.cache[coin_field] = get_perp_asset_info(
                    self.info, coin_field
                )
        return self.cache[coin_field]

    def invalidate(self, coin_field: str = None):
        """Clear cache for asset or all assets"""
        if coin_field:
            self.cache.pop(coin_field, None)
        else:
            self.cache.clear()
```

Refresh cache periodically or when orders get rejected.

## Troubleshooting

### Common rejection reasons

**"Order has invalid size"** (lot size violation) — size is not a multiple of `10^(-szDecimals)`. Query `szDecimals` from metadata and floor-truncate order size accordingly.

**"Price must be divisible by tick size"** (price precision) — price has too many significant figures (>5) or exceeds `MAX_DECIMALS - szDecimals` decimal places. Use the `format_price` function above.

**"Order must have minimum value of 10 USDC"** — below minimum notional value. Increase order size. Exception: exactly closing a position with reduce-only bypasses this minimum.

**"User or API Wallet 0x1a2B3c4D5e6F7a8B9c0D1e2F3a4B5c6D7e8F9a0b does not exist"** — often caused by trailing zeros in price or size strings during signing. For example, `"0.2300"` fails where `"0.23"` succeeds. If you build order wire messages manually, use `Decimal.normalize()` to strip trailing zeros before signing. The SDK's `float_to_wire()` handles this automatically.

**"Asset not found"** — wrong coin field format (`@index` for spot, `SYMBOL` for perps) or delisted asset.

**"Insufficient balance"** — not a precision issue, but verify wallet has enough collateral before placing orders.

## Summary

Query `szDecimals` from asset metadata to calculate valid order sizes dynamically—never hardcode precision. Floor-truncate sizes to `szDecimals` decimal places. Prices must have ≤5 significant figures with max `MAX_DECIMALS - szDecimals` decimal places (where `MAX_DECIMALS` is 6 for perps, 8 for spot); integer prices are always valid. Support all asset formats (`@index`, `PAIR/USDC`, `SYMBOL`), validate minimum notional (\$10 USDC), and cache metadata to reduce API calls.

The complete implementation is available in the [Chainstack Hyperliquid trading bot repository](https://github.com/chainstacklabs/hyperliquid-trading-bot/tree/main/learning_examples).

## Related resources

* [Copy trading with spot orders](/docs/hyperliquid-copy-trading-websocket) — Build a copy trading bot that mirrors spot trades
* [TWAP order execution](/docs/hyperliquid-twap-orders) — Place and monitor TWAP orders on Hyperliquid
* [Authentication guide](/docs/hyperliquid-authentication-guide) — Authenticate with Hyperliquid exchange API
* [API reference](/reference/hyperliquid-getting-started) — Explore the complete Hyperliquid API reference
