# SDK-007 — HL Account Setup & Balance Check

**Status:** Complete
**Date:** 2026-03-13
**Location:** `hyperscaled-sdk/`

---

## Summary

Implemented the account-management surface for configuring the SDK against a Hyperliquid wallet and checking whether that wallet meets the minimum funded-account requirement ($1,000). This mirrors the balance-gating behavior already present in the Chrome extension, but exposes it in both the Python SDK and CLI.

- `client.account.setup(wallet_address)` validates and persists the HL wallet to local config
- `client.account.check_balance()` queries the Hyperliquid info API and returns balance, meets_minimum, and minimum_required
- `client.account.watch_balance(callback)` continuously polls balance and invokes the callback on each update
- `hyperscaled account check` and `hyperscaled account check --json` CLI commands
- `BalanceStatus` Pydantic model and `MINIMUM_BALANCE` constant for reuse across SDK surfaces
- 33 mock-based tests covering healthy balance, insufficient balance, edge cases, missing wallet config, and Hyperliquid API failure handling

---

## What Was Built

### New Files

```
hyperscaled-sdk/
└── planning/
    └── reports/
        └── SDK-007.md         # This report
```

### Modified Files

| File | Change |
|------|--------|
| `hyperscaled/models/account.py` | Added `BalanceStatus` model and `MINIMUM_BALANCE` constant ($1,000) |
| `hyperscaled/models/__init__.py` | Exported `BalanceStatus` and `MINIMUM_BALANCE` |
| `hyperscaled/sdk/account.py` | Extended `AccountClient` with `setup()`, `check_balance()`, `watch_balance()`, and Hyperliquid info API integration |
| `hyperscaled/cli/account.py` | Replaced stubs with working `account check` and `account check --json` commands; cleaned up unused `fund` and `status` stubs |
| `tests/test_account.py` | Expanded from 11 SDK-006 tests to 33 tests covering SDK-007 setup, balance check, watch, and CLI surfaces |

---

## Details

### 1. `BalanceStatus` model

New Pydantic model representing the result of a balance check:

```python
class BalanceStatus(BaseModel):
    balance: Decimal
    meets_minimum: bool
    minimum_required: Decimal = MINIMUM_BALANCE  # Decimal("1000.00")
```

`MINIMUM_BALANCE` is defined as a module-level constant in `models/account.py` so it can be reused by SDK-008 pre-trade gating and any future balance-related logic without duplicating the threshold.

### 2. `AccountClient.setup()`

Validates the wallet address using the shared `is_valid_hl_address()` helper from SDK-006, then persists it to local config:

```python
async def setup_async(self, wallet_address: str) -> None:
    if not self.validate_wallet(wallet_address):
        raise ValueError(...)
    self._client.config.set_value("wallet.hl_address", wallet_address)
    self._client.config.save()
```

Raises `ValueError` on invalid input so the caller (CLI or script) can decide how to surface the error.

### 3. `AccountClient.check_balance()`

Queries the Hyperliquid info API (`clearinghouseState`) for the wallet's USDC account equity:

```python
async def check_balance_async(self, wallet_address: str | None = None) -> BalanceStatus:
    resolved = self._resolve_wallet(wallet_address)
    balance = await self._fetch_hl_balance(resolved)
    return BalanceStatus(balance=balance, meets_minimum=balance >= MINIMUM_BALANCE)
```

Behavior:

- Uses the configured HL wallet unless one is explicitly overridden
- Returns a structured `BalanceStatus` with the exact balance, a boolean `meets_minimum`, and the threshold
- Raises `HyperscaledError` for missing wallet config, HTTP errors, network failures, and unexpected response shapes

### 4. `AccountClient.watch_balance()`

Continuously polls balance and invokes a callback on each update:

```python
async def watch_balance(
    self,
    callback: Callable[[BalanceStatus], Any | Awaitable[Any]],
    *,
    wallet_address: str | None = None,
    poll_interval: float = 5.0,
) -> None:
```

Supports both sync and async callbacks. Runs until the task is cancelled via `asyncio.CancelledError`.

### 5. CLI commands

`hyperscaled account check` prints a human-readable pass/fail result:

```
Balance:          $1,250.42
Minimum required: $1,000.00
Status:           PASS
```

`hyperscaled account check --json` outputs machine-readable JSON:

```json
{
  "balance": "1250.42",
  "meets_minimum": true,
  "minimum_required": "1000.00"
}
```

`hyperscaled account check --wallet 0x...` allows overriding the configured wallet for a one-off check.

### 6. Reusability for downstream tickets

The `check_balance_async()` method is designed to be called by SDK-008 (funded account purchase) and future trade submission flows. When the balance is below the minimum, those flows should raise `InsufficientBalanceError` (already defined in `exceptions.py`) using the `BalanceStatus` result rather than duplicating the Hyperliquid API call or threshold logic.

### 7. Test coverage

33 tests organized across five test classes:

| Class | Tests | Coverage |
|-------|-------|----------|
| `test_is_valid_hl_address` (parametrized) | 9 | SDK-006 wallet format validation |
| `TestAccountCLI` | 2 | CLI `account setup` success and failure |
| `TestAccountSetupSDK` | 3 | SDK `setup()` valid, invalid, and whitespace addresses |
| `TestCheckBalance` | 9 | Healthy balance, insufficient balance, exact minimum boundary, zero balance, missing wallet, explicit wallet override, HTTP error, network error, unexpected response shape |
| `TestWatchBalance` | 3 | Async callback invocation, sync callback support, missing wallet |
| `TestAccountCheckCLI` | 6 | CLI check pass/fail, JSON output, missing wallet, API failure, wallet override |

All tests are mock-based with no live Hyperliquid API or backend dependencies.

---

## Acceptance Criteria Status

| Criterion | Status |
|-----------|--------|
| `client.account.setup(wallet_address)` validates and saves the HL wallet to config | Pass |
| `client.account.check_balance()` returns `balance`, `meets_minimum`, and `minimum_required` | Pass |
| `hyperscaled account setup` writes the wallet only if validation passes | Pass |
| `hyperscaled account check` prints a clear pass/fail result and current balance | Pass |
| `watch_balance(callback)` continuously emits updated balance states until cancelled | Pass |
| Trade and registration flows can reuse the same balance-check helper instead of duplicating logic | Pass |
| Mock-based tests cover healthy balance, insufficient balance, missing wallet config, and Hyperliquid API failure handling | Pass |

---

## Verification

Verification was run in the repo virtual environment at `hyperscaled-sdk/.venv`.

```bash
source .venv/bin/activate
ruff format hyperscaled/sdk/account.py hyperscaled/cli/account.py hyperscaled/models/account.py hyperscaled/models/__init__.py tests/test_account.py
ruff check hyperscaled/sdk/account.py hyperscaled/cli/account.py hyperscaled/models/account.py hyperscaled/models/__init__.py tests/test_account.py
mypy hyperscaled/sdk/account.py hyperscaled/cli/account.py hyperscaled/models/account.py hyperscaled/models/__init__.py
pytest -v
```

### Results

```
ruff format ...   → 5 files left unchanged
ruff check ...    → All checks passed!
mypy ...          → Success: no issues found in 4 source files
pytest -v ...     → 153 passed, 1 warning in 1.85s
```

### Warning observed

`pytest` still reports one existing runtime warning in `tests/test_client.py`:

- `TestSyncHelpers.test_run_sync_raises_in_running_loop`
- warning: `coroutine 'sleep' was never awaited`

This warning was not introduced by the SDK-007 changes.

---

## What's Next

SDK-007 now cleanly unblocks:

- **SDK-008** — Funded account purchase via x402 payment (reuses `check_balance()` for balance gating and `InsufficientBalanceError` for pre-trade rejection)
- **SDK-009** — Registration status polling (builds on top of the registration flow from SDK-008)
