# SDK-008 — Funded Account Purchase (SDK + CLI)

**Status:** In Progress
**Date:** 2026-03-13
**Branch:** `sdk-008/funded-account-purchase`
**Location:** `hyperscaled-sdk/`

---

## Summary

Implemented the first end-to-end funded-account purchase path in the SDK and CLI.

This work wires the SDK into the current `hyperscaled` app purchase flow by:

- validating Hyperliquid wallet input
- reusing the shared `$1,000` HL balance gate from `SDK-007`
- resolving the selected miner pricing tier
- initiating x402 payment against the Hyperscaled registration endpoint
- submitting the paid registration request
- returning structured purchase result data in the SDK and CLI

During validation against the latest `hyperscaled` and `vanta-network` code, the purchase flow required a follow-up alignment pass so the SDK matched the current app/backend contract rather than the earlier planning assumptions.

---

## What Was Built

### New Files

```text
hyperscaled-sdk/
├── hyperscaled/
│   └── sdk/
│       └── register.py        # RegisterClient purchase flow
└── planning/
    └── reports/
        └── SDK-008.md         # This report
```

### Modified Files

| File | Change |
|------|--------|
| `hyperscaled/sdk/client.py` | Wired `client.register` as a real lazy-loaded sub-client |
| `hyperscaled/cli/register.py` | Replaced the preflight stub with a real interactive purchase flow |
| `hyperscaled/models/registration.py` | Expanded registration result modeling for purchase responses |
| `hyperscaled/exceptions.py` | Added purchase-related exception types (`PaymentError`, `RegistrationError`, `InvalidMinerError`, `UnsupportedAccountSizeError`) |
| `pyproject.toml` | Added optional `x402` dependency group used by the SDK purchase flow |
| `tests/test_client.py` | Updated lazy sub-client coverage now that `client.register` is implemented |
| `tests/test_register.py` | Added SDK + CLI coverage for purchase success, validation failures, payment errors, and contract-aligned request behavior |

---

## Details

### 1. `RegisterClient` purchase flow

`hyperscaled/sdk/register.py` now provides a real `RegisterClient` with:

- `purchase_async(...)`
- `purchase(...)`

The implemented purchase path performs:

1. HL wallet validation
2. optional payout wallet validation
3. shared HL balance check via `client.account.check_balance_async()`
4. miner tier lookup using the shared miner catalog client
5. initial `POST /api/register` request expecting `402 Payment Required`
6. x402 payment signing
7. paid retry to `POST /api/register`
8. structured purchase result parsing

This keeps registration aligned with the rest of the SDK:

- sync methods work outside an event loop
- async methods are directly awaitable
- balance gating reuses existing SDK-007 logic instead of duplicating Hyperliquid calls

### 2. Interactive CLI purchase flow

`hyperscaled/cli/register.py` now provides a real purchase surface instead of the earlier placeholder.

The CLI currently:

- resolves the HL wallet from `--hl-wallet` or config
- shows the selected miner pricing tiers
- asks for purchase confirmation
- invokes `client.register.purchase(...)`
- renders structured success or error output

Supported entry points:

- `hyperscaled register --miner <slug> --size <account_size> --email <email>`
- `hyperscaled register purchase --miner <slug> --size <account_size> --email <email>`

### 3. Error handling

Purchase-related failures now use structured SDK errors:

- `InvalidMinerError`
- `UnsupportedAccountSizeError`
- `PaymentError`
- `RegistrationError`
- `InsufficientBalanceError` (reused from existing rule/balance logic)

This allows the CLI to distinguish:

- validation failures
- low-balance preflight failures
- x402 signing/setup failures
- backend registration failures

without collapsing everything into generic `HyperscaledError`.

### 4. Current-contract alignment pass

While validating the implementation against the latest `hyperscaled` app and current `vanta-network` integration points, several assumptions from the earlier SDK-008 shape no longer matched the live app/backend contract.

The SDK was adjusted to match the current app behavior in these ways:

- `email` is now required for registration requests
- `registration_id` is no longer fabricated in SDK code when the backend does not return one
- `registration_id` is now optional in the model so current backend responses can be represented honestly
- `payoutAddress` is only sent when explicitly supplied, allowing the backend to default to the x402 payer wallet
- CLI output only shows a registration ID when one actually exists

This keeps the purchase path usable against the current app contract while leaving room for a future dedicated registration-status surface.

### 5. Relationship to current backend behavior

The SDK purchase result is currently aligned to the live `hyperscaled` app response shape:

```json
{
  "status": "registered" | "pending",
  "message": "...",
  "txHash": "0x..."
}
```

Important current-state notes:

- the backend currently requires `email`
- the backend does not currently return `registration_id`
- the backend can default payout destination from the x402 payer wallet
- status polling is not yet part of the purchase result itself; that remains follow-up work

---

## Acceptance Criteria Status

| Criterion | Status |
|-----------|--------|
| `client.register.purchase(...)` validates wallet input before registration call | Pass |
| Purchase fails fast with `InsufficientBalanceError` when HL balance is below `$1,000` | Pass |
| Purchase resolves miner pricing/account size before payment | Pass |
| Successful purchase performs the two-step registration request (`402` then paid retry) | Pass |
| CLI registration flow is interactive and shows price/result information | Pass |
| Purchase result modeling matches the current backend more closely than the original planning shape | Pass |
| Mock-based tests cover happy path and failure branches without live payment/backend dependencies | Pass |
| Dedicated registration polling/status flow | Partial / follow-up |

---

## Verification

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

```bash
source .venv/bin/activate
pytest -q tests/test_register.py tests/test_miners.py
ruff check hyperscaled/sdk/register.py hyperscaled/cli/register.py hyperscaled/models/registration.py tests/test_register.py
```

### Results

```text
pytest -q tests/test_register.py tests/test_miners.py  → 29 passed in 0.21s
ruff check ...                                         → All checks passed!
```

---

## Test Coverage

| Test Area | Coverage |
|-----------|----------|
| SDK validation | invalid HL wallet, invalid payout wallet, missing email |
| Balance gating | insufficient HL balance before payment |
| Catalog resolution | invalid miner slug, unsupported account size |
| Payment setup | missing private key, mocked x402 signing path |
| Purchase success | happy-path paid registration response |
| Purchase request shape | verifies `email` is present and `payoutAddress` is omitted when unspecified |
| Backend failure handling | paid request returning `500` surfaces `RegistrationError` |
| Sync SDK usage | `client.register.purchase(...)` outside an event loop |
| CLI purchase flow | happy path, wallet override, low balance, payment error, invalid miner |

---

## Open Follow-up Items

The current implementation is a strong first pass, but a few items remain for later refinement:

- add a dedicated status/polling client aligned with the current `/api/registration-status` route
- decide whether purchase results should eventually use a dedicated model separate from registration-status results
- verify the real Python x402 client end-to-end against the live app contract instead of only mocked signing behavior
- revisit response modeling if the backend starts returning `registration_id`

---

## Why This Shape Was Chosen

The planning docs originally described a purchase result that included `registration_id` and estimated-time metadata. The current app/backend implementation does not yet expose that shape consistently.

Rather than hard-coding planned fields that are not actually returned today, the SDK now favors the live contract:

- require the fields the backend actually needs
- surface the fields the backend actually returns
- avoid inventing identifiers in SDK code

This keeps the SDK honest, reduces future cleanup, and makes later `SDK-009` status work easier to build on top of the real backend state model.
