# SDK-012 — Pre-submission Local Rule Enforcement

**Status:** Complete
**Date:** 2026-03-18
**Branch:** `sdk-012/pre-submission-local-rule-enforcement`
**Location:** `hyperscaled-sdk/`

---

## Summary

Implemented validator-backed pre-submission trade validation so SDK and CLI users are blocked locally before any Hyperliquid order submission when a trade violates currently known Vanta constraints.

This work adds:

- a real `RulesClient` in `hyperscaled/sdk/rules.py`
- lazy loading for `client.rules`
- `client.trade.submit()` integration via the existing `_pre_validate()` seam from SDK-010
- validator-backed pair discovery through `GET /trade-pairs`
- validator-backed funded-account state reads through `GET /hl/<hl_address>/dashboard`
- typed rule failures for unsupported pair, temporarily halted pair, leverage, exposure, account status, and drawdown breach
- CLI support for `hyperscaled rules list`, `hyperscaled rules check`, and `hyperscaled trade submit --strict`

The implementation intentionally isolates uncertain contract details inside the rules adapter layer so future backend/API changes do not require reworking the trading flow.

---

## What Was Built

### New Files

```text
hyperscaled-sdk/
├── tests/
│   └── test_rules.py             # SDK-012 rules client and validation coverage
└── planning/
    └── reports/
        └── SDK-012.md            # This report
```

### Modified Files

| File | Change |
|------|--------|
| `hyperscaled/sdk/rules.py` | Replaced stub with validator-backed `RulesClient`, rule listing, and trade validation |
| `hyperscaled/sdk/client.py` | Added lazy-loaded `rules` property |
| `hyperscaled/sdk/trading.py` | Connected `_pre_validate()` to `client.rules.validate_trade_async()` and preserved early local preconditions |
| `hyperscaled/exceptions.py` | Added `TemporarilyHaltedPairError`, plus compatibility aliases for rule payload naming |
| `hyperscaled/cli/trade.py` | Added `--strict` support for local rule violations |
| `hyperscaled/cli/rules.py` | Implemented `rules list` and `rules check` commands |
| `tests/test_client.py` | Updated lazy-loading coverage for `client.rules` |
| `tests/test_trading.py` | Updated trade tests to use the real rules seam |
| `tests/test_exceptions.py` | Added coverage for the new halted-pair error |

---

## Details

### 1. `RulesClient`

`hyperscaled/sdk/rules.py` now owns all validator-specific rule fetching and mapping:

1. Fetch allowed trade pairs from `GET /trade-pairs`
2. Match user input against validator pair metadata
3. Fetch account/dashboard state from `GET /hl/<hl_address>/dashboard`
4. Compute requested notional, requested leverage, and projected exposure locally
5. Raise a typed SDK exception before any Hyperliquid order call when a rule fails

This keeps the rest of the SDK dependent on a stable internal validation interface instead of raw backend payloads.

### 2. Pair validation source of truth

The trade path no longer relies on the old six-pair hardcoded allowlist from SDK-010. Instead, pair support now comes from the validator `trade-pairs` endpoint.

Each allowed pair entry is normalized into SDK-facing display values where possible:

- crypto examples: `BTCUSD` / `BTC/USD` -> `BTC-USDC`
- non-crypto examples are normalized from validator strings as best-effort

### 3. Account-state validation

The validator dashboard payload is used to block trades when the account is already not tradeable:

- non-active subaccount status -> `AccountSuspendedError`
- drawdown breach / fully used drawdown budget -> `DrawdownBreachError`

### 4. Leverage and exposure validation

The implementation uses the current Vanta validator math discovered in the backend codebase:

- per-pair max leverage comes from `/trade-pairs`
- account-level leverage cap is derived from asset class:
  - crypto = `5`
  - forex = `20`
  - indices = `10`
  - equities = `2`
- challenge-mode limits divide both pair and account caps by `4`

Requested notional is computed as:

```text
requested_notional = size * validation_price
requested_leverage = requested_notional / funded_balance
projected_exposure = current_exposure + requested_notional
```

### 5. CLI behavior

`hyperscaled trade submit --strict` now exits with code `1` on rule violations.

`hyperscaled rules list` exposes the currently fetched validator rule summary and `hyperscaled rules check` runs the same validation path used by trade submission.

---

## Acceptance Criteria Status

| Criterion | Status |
|-----------|--------|
| `client.rules.validate_trade(...)` returns a structured `TradeValidation` result for valid trades | Pass |
| `client.trade.submit(...)` blocks invalid submissions before any Hyperliquid call is made | Pass |
| Unsupported pair, leverage, exposure, account-status, and drawdown failures raise typed errors | Pass |
| CLI validation path exists and `trade submit --strict` exits non-zero on rule violations | Pass |
| Mock-based tests cover success and rule-failure branches | Pass |

---

## Verification

```bash
python3 -m pytest tests/test_rules.py tests/test_trading.py tests/test_client.py tests/test_exceptions.py -q
python3 -m pytest tests/test_imports.py -q
```

Results:

- `86 passed` in the targeted SDK-012 test set
- `4 passed` in import smoke tests
- no linter errors on edited files

---

## Caveats / Follow-Up Notes

These caveats are important for future developers:

### 1. `rule_id` values are provisional

The backend contract for machine-readable validation identifiers was not finalized at implementation time. The current `rule_id` values in `hyperscaled/sdk/rules.py` are SDK-local placeholders designed to be easy to replace later.

If/when the backend publishes canonical `rule_id` / `code` values, update the mapping constants in `RulesClient` rather than changing the trading flow.

### 2. Error field naming is intentionally backward-compatible

The older SDK exception shape used `limit` and `actual_value`, while the V2 design doc prefers names like `current_value` and `allowed_value`.

To avoid breaking existing SDK code/tests while keeping future migration easy:

- the stored exception fields remain `limit` and `actual_value`
- compatibility aliases `current_value` and `allowed_value` were added on `RuleViolationError`

If the SDK standardizes on the V2 naming later, that migration can happen centrally in the exception/model layer.

### 3. The current implementation uses validator endpoints directly

This ticket currently reads from:

- `GET /trade-pairs`
- `GET /hl/<hl_address>/dashboard`

This matches the currently discoverable validator contract in the referenced Vanta branch. However, product discussion suggested the hosted entity miner API may become the longer-term integration surface for dashboard/rule-adjacent data.

If that happens, the migration should primarily affect `RulesClient`, not `TradingClient`.

### 4. Halted-vs-unsupported is still an inferred distinction

`TemporarilyHaltedPairError` exists because the product/backend discussion explicitly wanted a separate error from unsupported pair.

However, the currently available pair endpoint only returns the allowed set; it does not expose a first-class "halted" payload with machine-readable reason data. The current implementation therefore infers a halted/unavailable state from "well-formed pair not present in allowed list".

If the backend later exposes explicit halted metadata, update this mapping logic in `RulesClient`.

### 5. Market-order validation currently assumes Hyperliquid mid-price support for crypto pairs

When no explicit price is supplied, `RulesClient` fetches Hyperliquid mids and derives notional from that value. This is suitable for the current Hyperliquid crypto trading scope, but it is not yet a generalized cross-asset pricing abstraction.

If non-crypto trade submission becomes part of the SDK, price sourcing for validation should be revisited.

### 6. The old `$1,000` balance gate is intentionally not enforced

Based on product/dev clarification during implementation, the historical `$1,000` minimum balance rule was treated as removed / non-required for SDK-012. It was therefore not added to the new validation layer.

If that rule returns later, it should be implemented in `RulesClient` as another adapter-mapped rule rather than as a special case in `TradingClient`.

### 7. `hyperscaled/sdk/pairs.py` still exists as a legacy utility

SDK-010 introduced static pair helpers for the original six-pair scope. SDK-012 removes that hardcoded list from the submission path, but the helper module still exists for normalization utilities and older tests.

If a future cleanup ticket wants to remove static pair assumptions entirely, audit `hyperscaled/sdk/pairs.py` and any tests importing `validate_pair()`.

---

## Open Follow-Up Items

- Replace provisional SDK-local `rule_id` values with canonical backend identifiers once defined
- Decide whether the final stable source for rule/account-state reads should remain validator endpoints or move to the entity miner API
- Add first-class backend support for halted-pair metadata if available
- Add payout and rule-violation-history reads when those backend surfaces are ready
- Revisit validation price sourcing if non-crypto trading is added
