# SDK-005 — Entity Miner Discovery (Narrowed Catalog Scope)

**Status:** Complete
**Date:** 2026-03-05
**Branch:** `sdk-005/miner-discovery` (off `main`)
**Location:** `hyperscaled-sdk/`

---

## Summary

Implemented the first real HTTP-backed SDK feature: entity miner discovery via the Hyperscaled miner catalog.

This ticket was intentionally narrowed during implementation so it only covers miner-owned catalog metadata:

- miner name and slug
- pricing tiers
- payout cadence
- per-tier profit split
- available account sizes
- optional brand color

It does **not** include supported trading pairs or leverage limits. Those remain Vanta-owned/global rule data and should be surfaced later through rules/account-oriented SDK surfaces rather than bundled into miner discovery.

---

## What Was Built

### New Files

```
hyperscaled-sdk/
├── hyperscaled/
│   └── sdk/
│       └── miners.py          # MinersClient, payload normalization, sync/async support
└── tests/
    └── test_miners.py         # SDK + CLI tests for miner discovery
```

### Modified Files

| File | Change |
|------|--------|
| `hyperscaled/models/miner.py` | Narrowed the miner catalog models: moved `profit_split` onto `PricingTier`, removed rule-oriented fields from `EntityMiner`, added optional `brand_color` |
| `hyperscaled/sdk/client.py` | Wired `client.miners` as a real lazy-loaded sub-client instead of a stub |
| `hyperscaled/cli/miners.py` | Replaced CLI stubs with working `list`, `info`, `compare`, and `--json` commands |
| `tests/test_models.py` | Updated miner model tests for the narrowed catalog contract |
| `tests/test_client.py` | Updated lazy sub-client tests now that `client.miners` is implemented |
| `tests/test_imports.py` | Added `hyperscaled.sdk.miners` to import checks |
| `README.md` | Updated examples so miner discovery is shown as implemented |

---

## Details

### 1. Narrowed miner catalog model

The original sprint planning for `SDK-005` mixed miner-owned catalog metadata with Vanta-owned rule data. The implemented SDK shape follows the narrower catalog scope:

```python
class PricingTier(BaseModel):
    account_size: int
    cost: Decimal
    profit_split: ProfitSplit

class ProfitSplit(BaseModel):
    trader_pct: int
    miner_pct: int

class EntityMiner(BaseModel):
    name: str
    slug: str
    pricing_tiers: list[PricingTier]
    payout_cadence: str
    available_account_sizes: list[int]
    brand_color: str | None = None
```

This keeps the miner object aligned with the current backend ownership boundaries:

- **Hyperscaled catalog data:** tiers, payout cadence, branding
- **Vanta rule data:** supported pairs, leverage limits, rule enforcement

### 2. Real `MinersClient`

Added `hyperscaled/sdk/miners.py` with:

- `list_all_async()`
- `get_async(slug)`
- `compare_async(slugs=None)`
- sync-friendly wrappers `list_all()`, `get()`, and `compare()`

The sync wrappers behave like the rest of the SDK contract:

- outside an event loop they run immediately via `_run_sync()`
- inside an event loop they return a coroutine for `await`

### 3. Backend payload normalization

The miner client accepts both:

- the **intended stable SDK shape** (`pricing_tiers`, `payout_cadence`, `brand_color`)
- the **current DB-backed/internal backend shape** already emerging in the Hyperscaled backend work (`tiers`, `payoutCadenceDays`, `color`, integer `profitSplit`)

This reduces churn while the backend catalog contract settles.

Normalization includes:

- `tiers` → `pricing_tiers`
- `priceUsdc` / `price_usdc` → `cost`
- integer `profitSplit` → `ProfitSplit(trader_pct, miner_pct)`
- `payoutCadenceDays` / `payout_cadence_days` → normalized strings like `weekly`
- derived `available_account_sizes` from tiers when the backend does not send them explicitly

### 4. CLI implementation

Implemented the `miners` CLI group:

- `hyperscaled miners list`
- `hyperscaled miners info <slug>`
- `hyperscaled miners compare`
- `--json` on all commands

The human-readable output is catalog-focused:

- list/compare show slug, profit split summary, payout cadence, and account sizes
- info shows detailed pricing tiers with per-tier profit split

If profit split differs across tiers, the CLI summarizes it as `Varies by tier`.

### 5. Error handling

The miner client raises `HyperscaledError` for:

- network failures
- unexpected response shapes
- 404 miner-not-found responses
- non-2xx backend errors

This keeps `SDK-005` consistent with the rest of the SDK's error surface without prematurely introducing a new discovery-specific exception hierarchy.

---

## Acceptance Criteria — All Met

| Criterion | Status |
|-----------|--------|
| `hyperscaled miners list` displays a formatted table of all miners | Pass |
| `hyperscaled miners info <slug>` shows full detail for one miner | Pass |
| `hyperscaled miners compare` shows a side-by-side comparison table | Pass |
| `--json` flag outputs raw JSON on all commands | Pass |
| `client.miners.list_all()` and `client.miners.get(slug)` work from Python | Pass |
| Graceful error handling: API unreachable, miner not found (404) | Pass |
| Mock-based tests for all SDK methods and CLI commands (no live API dependency) | Pass |

---

## Verification

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

```bash
.venv/bin/pytest -q
.venv/bin/ruff check .
.venv/bin/mypy hyperscaled tests
```

### Results

```
pytest -q                    → 116 passed, 1 warning
ruff check .                 → All checks passed!
mypy hyperscaled tests       → Success: no issues found in 42 source files
```

### 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 predates `SDK-005` and was not introduced by the miner discovery work.

---

## Test Coverage

| Test File | Test Group | Count | What's Covered |
|-----------|------------|-------|----------------|
| `test_miners.py` | SDK normalization | 2 | Stable payload shape, current DB-backed/internal payload shape |
| | Sync SDK usage | 1 | `client.miners.list_all()` outside an event loop |
| | Error handling | 1 | 404 miner-not-found case |
| | Compare behavior | 1 | `compare_async()` fetches explicit slugs |
| | CLI commands | 3 | `miners list`, `miners info --json`, `miners compare` |
| `test_models.py` | Miner models | Updated existing tests | Tier-level `profit_split`, narrowed `EntityMiner`, optional `brand_color` |
| `test_client.py` | Lazy sub-clients | Updated existing tests | `client.miners` is now lazy-loaded and cached instead of stubbed |

---

## Why The Scope Was Narrowed

Reviewing the current `hyperscaled` and `vanta-network` work showed that the broader original `SDK-005` concept was mixing two concerns:

- **Miner catalog data** owned by Hyperscaled
- **Trading rule data** owned by Vanta/global rule logic

The current architecture is cleaner if `SDK-005` only handles miner discovery/catalog:

- list miners
- fetch miner by slug
- compare miners
- expose tiers, cadence, splits, and sizes

Then later SDK work can expose:

- supported pairs
- leverage limits
- rule validation
- account/risk surfaces

through Vanta-backed APIs where those concepts actually live.

---

## What's Next

The next logical step is to align the backend catalog contract that `MinersClient` should target long-term:

- stable endpoint paths (`/api/v1/miners`, `/api/v1/miners/{slug}`)
- canonical field names
- cadence representation
- per-tier profit split representation
- optional branding fields

Once that contract is locked, later tickets can build on top of it for registration and Vanta-backed rule/account flows.
