# CLI & SDK Guide

## Overview

Clore.ai provides a **REST API** that enables full programmatic access to the GPU marketplace — listing servers, creating orders, monitoring deployments, and canceling rentals.

> **Note:** There is no official CLI binary at this time. All automation is done directly via the REST API using tools like `curl`, Python, or Node.js.

**Base URL:** `https://api.clore.ai/v1`

**Response format:** JSON. Every response includes a `code` field indicating the status.

***

## Authentication

Generate your API key from the [Clore.ai Dashboard](https://clore.ai):

1. Log in to your account
2. Navigate to **API** section in settings
3. Generate and copy your API key

**Header format:**

```
auth: YOUR_API_KEY
```

> ⚠️ **Important:** The auth header is `auth`, **not** `Authorization: Bearer`. Using the wrong format will return code `3` (Invalid API Token).

**Example:**

```bash
curl -H 'auth: YOUR_API_KEY' 'https://api.clore.ai/v1/marketplace'
```

***

## Quick Start

### List Marketplace Servers

Browse all available GPU servers:

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/marketplace'
```

**Response:**

```json
{
  "servers": [
    {
      "id": 6,
      "owner": 4,
      "mrl": 73,
      "price": {
        "on_demand": { "bitcoin": 0.00001 },
        "spot": { "bitcoin": 0.000001 }
      },
      "rented": false,
      "specs": {
        "cpu": "Intel Core i9-11900",
        "ram": 62.67,
        "gpu": "1x NVIDIA GeForce GTX 1080 Ti",
        "gpuram": 11,
        "net": { "up": 26.38, "down": 118.42, "cc": "CZ" }
      }
    }
  ],
  "my_servers": [1, 2, 4],
  "code": 0
}
```

***

### Get Your Orders

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/my_orders'
```

Include completed/expired orders:

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/my_orders?return_completed=true'
```

***

### Create an Order (On-Demand)

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "currency": "bitcoin",
    "image": "cloreai/ubuntu20.04-jupyter",
    "renting_server": 6,
    "type": "on-demand",
    "ports": {
      "22": "tcp",
      "8888": "http"
    },
    "ssh_password": "YourSSHPassword123",
    "jupyter_token": "YourJupyterToken123"
  }' \
  'https://api.clore.ai/v1/create_order'
```

**Response:**

```json
{ "code": 0 }
```

***

### Create a Spot Order

Spot orders are cheaper but can be outbid. You set your price per day:

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "currency": "bitcoin",
    "image": "cloreai/ubuntu20.04-jupyter",
    "renting_server": 6,
    "type": "spot",
    "spotprice": 0.000005,
    "ports": {
      "22": "tcp",
      "8888": "http"
    },
    "ssh_password": "YourSSHPassword123"
  }' \
  'https://api.clore.ai/v1/create_order'
```

***

### Check Order Status

```bash
curl -XGET \
  -H 'auth: YOUR_API_KEY' \
  'https://api.clore.ai/v1/my_orders'
```

Active orders include `pub_cluster` (hostnames) and `tcp_ports` for SSH access:

```json
{
  "id": 38,
  "pub_cluster": ["n1.c1.clorecloud.net", "n2.c1.clorecloud.net"],
  "tcp_ports": ["22:10000"],
  "http_port": "8888",
  "expired": false
}
```

SSH into your rented server:

```bash
ssh root@n1.c1.clorecloud.net -p 10000
```

***

### Cancel an Order

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "id": 38
  }' \
  'https://api.clore.ai/v1/cancel_order'
```

Optionally report an issue with the server:

```bash
curl -XPOST \
  -H 'auth: YOUR_API_KEY' \
  -H 'Content-type: application/json' \
  -d '{
    "id": 38,
    "issue": "GPU was overheating and throttling performance"
  }' \
  'https://api.clore.ai/v1/cancel_order'
```

***

## Python SDK

A lightweight wrapper using the `requests` library. Install it with:

```bash
pip install requests
```

### CloreClient Class

```python
import requests
import time

class CloreClient:
    """Simple Python SDK for Clore.ai REST API."""
    
    BASE_URL = "https://api.clore.ai/v1"
    
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({"auth": api_key})
    
    def _get(self, endpoint: str, params: dict = None) -> dict:
        url = f"{self.BASE_URL}/{endpoint}"
        response = self.session.get(url, params=params)
        response.raise_for_status()
        data = response.json()
        if data.get("code") != 0:
            raise CloreAPIError(data)
        return data
    
    def _post(self, endpoint: str, body: dict) -> dict:
        url = f"{self.BASE_URL}/{endpoint}"
        self.session.headers.update({"Content-type": "application/json"})
        response = self.session.post(url, json=body)
        response.raise_for_status()
        data = response.json()
        if data.get("code") != 0:
            raise CloreAPIError(data)
        return data
    
    def list_servers(self) -> list:
        """Get all available servers on the marketplace."""
        data = self._get("marketplace")
        return data["servers"]
    
    def get_server_details(self, server_id: int) -> dict:
        """Get spot marketplace info for a specific server."""
        data = self._get("spot_marketplace", params={"market": server_id})
        return data["market"]
    
    def create_order(
        self,
        server_id: int,
        image: str,
        order_type: str = "on-demand",
        currency: str = "bitcoin",
        spotprice: float = None,
        ports: dict = None,
        ssh_password: str = None,
        ssh_key: str = None,
        jupyter_token: str = None,
        env: dict = None,
        command: str = None,
    ) -> dict:
        """
        Create an on-demand or spot order.
        
        Args:
            server_id: ID of server to rent
            image: Docker image (e.g. 'cloreai/ubuntu20.04-jupyter')
            order_type: 'on-demand' or 'spot'
            currency: 'bitcoin' (default)
            spotprice: Required for spot orders — price per day in BTC
            ports: Port forwarding, e.g. {"22": "tcp", "8888": "http"}
            ssh_password: SSH password (alphanumeric chars only)
            ssh_key: SSH public key
            jupyter_token: Jupyter notebook token
            env: Environment variables dict
            command: Shell command to run after container start
        """
        if order_type == "spot" and spotprice is None:
            raise ValueError("spotprice is required for spot orders")
        
        body = {
            "currency": currency,
            "image": image,
            "renting_server": server_id,
            "type": order_type,
        }
        
        if spotprice is not None:
            body["spotprice"] = spotprice
        if ports:
            body["ports"] = ports
        if ssh_password:
            body["ssh_password"] = ssh_password
        if ssh_key:
            body["ssh_key"] = ssh_key
        if jupyter_token:
            body["jupyter_token"] = jupyter_token
        if env:
            body["env"] = env
        if command:
            body["command"] = command
        
        return self._post("create_order", body)
    
    def get_orders(self, include_completed: bool = False) -> list:
        """Get your active (and optionally completed) orders."""
        params = {"return_completed": "true"} if include_completed else {}
        data = self._get("my_orders", params=params)
        return data["orders"]
    
    def cancel_order(self, order_id: int, issue: str = None) -> dict:
        """Cancel an order. Optionally report an issue."""
        body = {"id": order_id}
        if issue:
            body["issue"] = issue
        return self._post("cancel_order", body)
    
    def get_wallets(self) -> list:
        """Get your wallets and balances."""
        data = self._get("wallets")
        return data["wallets"]
    
    def get_my_servers(self) -> list:
        """Get servers you are providing to the marketplace."""
        data = self._get("my_servers")
        return data["servers"]


class CloreAPIError(Exception):
    """Raised when the Clore API returns a non-zero code."""
    
    ERROR_CODES = {
        0: "Normal",
        1: "Database error",
        2: "Invalid input data",
        3: "Invalid API token",
        4: "Invalid endpoint",
        5: "Rate limit exceeded (1 req/sec)",
        6: "Error (see error field)",
    }
    
    def __init__(self, response: dict):
        self.code = response.get("code")
        self.error = response.get("error", "")
        message = self.ERROR_CODES.get(self.code, f"Unknown code {self.code}")
        if self.error:
            message = f"{message}: {self.error}"
        super().__init__(f"Clore API error {self.code}: {message}")
```

### Full Working Example

```python
import os
from clore_client import CloreClient, CloreAPIError

# Initialize client
client = CloreClient(api_key=os.environ["CLORE_API_KEY"])

# 1. Browse marketplace
servers = client.list_servers()
print(f"Found {len(servers)} servers on marketplace")

# 2. Filter available RTX 4090 servers
rtx4090_servers = [
    s for s in servers
    if "4090" in s["specs"].get("gpu", "") and not s["rented"]
]

if not rtx4090_servers:
    print("No available RTX 4090 servers found")
    exit(1)

# 3. Pick the cheapest one
cheapest = min(rtx4090_servers, key=lambda s: s["price"]["on_demand"]["bitcoin"])
print(f"Cheapest RTX 4090: server ID {cheapest['id']}, "
      f"price {cheapest['price']['on_demand']['bitcoin']:.8f} BTC/day")

# 4. Create an order
try:
    client.create_order(
        server_id=cheapest["id"],
        image="cloreai/ubuntu20.04-jupyter",
        order_type="on-demand",
        currency="bitcoin",
        ports={"22": "tcp", "8888": "http"},
        ssh_password="SecurePass123",
        jupyter_token="MyToken123",
    )
    print("Order created successfully!")
except CloreAPIError as e:
    print(f"Failed to create order: {e}")
    exit(1)

# 5. Check your orders
orders = client.get_orders()
for order in orders:
    if not order.get("expired"):
        cluster = order.get("pub_cluster", [])
        tcp = order.get("tcp_ports", [])
        print(f"Order {order['id']}: server {order['si']}")
        if cluster and tcp:
            ssh_port = tcp[0].split(":")[1]
            print(f"  SSH: ssh root@{cluster[0]} -p {ssh_port}")
```

***

## Node.js Example

Using the native `fetch` API (Node.js 18+):

```javascript
const BASE_URL = 'https://api.clore.ai/v1';

class CloreClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.defaultHeaders = {
      'auth': apiKey,
    };
  }

  async get(endpoint, params = {}) {
    const url = new URL(`${BASE_URL}/${endpoint}`);
    Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
    
    const res = await fetch(url.toString(), {
      headers: this.defaultHeaders,
    });
    
    const data = await res.json();
    if (data.code !== 0) {
      throw new Error(`Clore API error ${data.code}: ${data.error || 'unknown'}`);
    }
    return data;
  }

  async post(endpoint, body) {
    const res = await fetch(`${BASE_URL}/${endpoint}`, {
      method: 'POST',
      headers: {
        ...this.defaultHeaders,
        'Content-type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    
    const data = await res.json();
    if (data.code !== 0) {
      throw new Error(`Clore API error ${data.code}: ${data.error || 'unknown'}`);
    }
    return data;
  }

  async listServers() {
    const data = await this.get('marketplace');
    return data.servers;
  }

  async getOrders(includeCompleted = false) {
    const params = includeCompleted ? { return_completed: 'true' } : {};
    const data = await this.get('my_orders', params);
    return data.orders;
  }

  async createOrder({ serverId, image, type = 'on-demand', currency = 'bitcoin', spotprice, ports, sshPassword, jupyterToken, env, command }) {
    const body = {
      currency,
      image,
      renting_server: serverId,
      type,
      ...(spotprice && { spotprice }),
      ...(ports && { ports }),
      ...(sshPassword && { ssh_password: sshPassword }),
      ...(jupyterToken && { jupyter_token: jupyterToken }),
      ...(env && { env }),
      ...(command && { command }),
    };
    return this.post('create_order', body);
  }

  async cancelOrder(orderId, issue = null) {
    const body = { id: orderId };
    if (issue) body.issue = issue;
    return this.post('cancel_order', body);
  }
}

// Usage example
const client = new CloreClient(process.env.CLORE_API_KEY);

async function main() {
  // List marketplace
  const servers = await client.listServers();
  console.log(`Marketplace has ${servers.length} servers`);

  // Find available RTX 4090 servers
  const available = servers.filter(
    s => s.specs.gpu.includes('4090') && !s.rented
  );
  console.log(`Available RTX 4090 servers: ${available.length}`);

  // Get current orders
  const orders = await client.getOrders();
  const activeOrders = orders.filter(o => !o.expired);
  console.log(`Active orders: ${activeOrders.length}`);

  for (const order of activeOrders) {
    const host = order.pub_cluster?.[0];
    const portMapping = order.tcp_ports?.[0];
    if (host && portMapping) {
      const sshPort = portMapping.split(':')[1];
      console.log(`Order ${order.id} → ssh root@${host} -p ${sshPort}`);
    }
  }
}

main().catch(console.error);
```

***

## Common Workflows

### Find Cheapest RTX 4090 and Rent It

```python
from clore_client import CloreClient

client = CloreClient(api_key="YOUR_API_KEY")

def rent_cheapest_rtx4090(image="cloreai/ubuntu20.04-jupyter", ssh_password="SecurePass123"):
    servers = client.list_servers()
    
    # Filter: available RTX 4090
    candidates = [
        s for s in servers
        if "4090" in s["specs"].get("gpu", "")
        and not s["rented"]
    ]
    
    if not candidates:
        raise RuntimeError("No available RTX 4090 servers found")
    
    # Sort by on-demand BTC price
    candidates.sort(key=lambda s: s["price"]["on_demand"]["bitcoin"])
    best = candidates[0]
    
    price_btc = best["price"]["on_demand"]["bitcoin"]
    print(f"Renting server {best['id']}: {best['specs']['gpu']} @ {price_btc:.8f} BTC/day")
    
    client.create_order(
        server_id=best["id"],
        image=image,
        order_type="on-demand",
        currency="bitcoin",
        ports={"22": "tcp"},
        ssh_password=ssh_password,
    )
    
    print("Done! Check your orders for SSH connection details.")
    return best["id"]

rent_cheapest_rtx4090()
```

***

### Monitor My Orders

```python
import time
from clore_client import CloreClient

client = CloreClient(api_key="YOUR_API_KEY")

def monitor_orders(poll_interval_seconds=60):
    """Poll orders and print status updates."""
    print(f"Monitoring orders (polling every {poll_interval_seconds}s). Ctrl+C to stop.\n")
    
    while True:
        orders = client.get_orders(include_completed=False)
        active = [o for o in orders if not o.get("expired")]
        
        print(f"--- {len(active)} active order(s) ---")
        for order in active:
            cluster = order.get("pub_cluster", [])
            tcp = order.get("tcp_ports", [])
            spend = order.get("spend", 0)
            
            ssh_info = ""
            if cluster and tcp:
                port = tcp[0].split(":")[1]
                ssh_info = f" | SSH: {cluster[0]}:{port}"
            
            print(f"  Order {order['id']}: server {order['si']}"
                  f" | spent {spend:.8f} BTC{ssh_info}")
        
        if not active:
            print("  No active orders.")
        
        print()
        time.sleep(poll_interval_seconds)

monitor_orders()
```

***

### Auto-Rent When Price Drops Below X

```python
import time
from clore_client import CloreClient

client = CloreClient(api_key="YOUR_API_KEY")

def auto_rent_on_price_drop(
    gpu_model: str = "RTX 4090",
    max_price_btc: float = 0.00015,
    image: str = "cloreai/ubuntu20.04-jupyter",
    ssh_password: str = "SecurePass123",
    check_interval_seconds: int = 120,
):
    """
    Watch the marketplace and auto-rent a GPU when price drops below threshold.
    
    Args:
        gpu_model: GPU name to search for (case-insensitive)
        max_price_btc: Maximum acceptable price per day in BTC
        image: Docker image to deploy
        ssh_password: SSH password for the container
        check_interval_seconds: How often to check (respect rate limits!)
    """
    print(f"Watching for {gpu_model} at ≤ {max_price_btc:.8f} BTC/day...")
    
    while True:
        servers = client.list_servers()
        
        for server in servers:
            gpu = server["specs"].get("gpu", "")
            if gpu_model.lower() not in gpu.lower():
                continue
            if server["rented"]:
                continue
            
            price = server["price"]["on_demand"]["bitcoin"]
            if price <= max_price_btc:
                print(f"🎯 Found match! Server {server['id']}: {gpu} @ {price:.8f} BTC/day")
                
                try:
                    client.create_order(
                        server_id=server["id"],
                        image=image,
                        order_type="on-demand",
                        currency="bitcoin",
                        ports={"22": "tcp"},
                        ssh_password=ssh_password,
                        required_price=price,  # Lock in this price
                    )
                    print(f"✅ Order created for server {server['id']}!")
                    return server["id"]
                except Exception as e:
                    print(f"Failed to create order: {e}. Will retry...")
        
        print(f"No match yet. Checking again in {check_interval_seconds}s...")
        time.sleep(check_interval_seconds)

auto_rent_on_price_drop(gpu_model="4090", max_price_btc=0.00012)
```

***

## WebSocket

The Clore.ai REST API does not currently expose a WebSocket endpoint. For real-time monitoring, use polling with a reasonable interval (see rate limits below).

***

## Rate Limits

| Endpoint                           | Limit                    |
| ---------------------------------- | ------------------------ |
| Most endpoints                     | **1 request/second**     |
| `create_order`                     | **1 request/5 seconds**  |
| `set_spot_price` (price reduction) | Once per **600 seconds** |

**Rate limit response (code 5):**

```json
{ "code": 5 }
```

**Best practices:**

* Add `time.sleep(1)` between consecutive API calls
* For `create_order`, wait at least 5 seconds between requests
* Use polling intervals of 60+ seconds for monitoring loops
* Cache marketplace data locally if you need to query it frequently

**Python helper:**

```python
import time

def safe_api_call(fn, *args, delay=1.1, **kwargs):
    """Wrap an API call with rate limit safety."""
    result = fn(*args, **kwargs)
    time.sleep(delay)
    return result
```

***

## Error Handling

Every API response includes a `code` field. A value of `0` means success.

### Error Codes

| Code | Meaning                               | Action                               |
| ---- | ------------------------------------- | ------------------------------------ |
| `0`  | Success                               | —                                    |
| `1`  | Database error                        | Retry after a delay                  |
| `2`  | Invalid input data                    | Check your request body/parameters   |
| `3`  | Invalid API token                     | Verify your API key in the dashboard |
| `4`  | Invalid endpoint                      | Check the endpoint URL               |
| `5`  | Rate limit exceeded (1 req/sec)       | Add delays between requests          |
| `6`  | Application error (see `error` field) | Read the `error` field for details   |

### Code 6 Sub-errors

| `error` value                 | Meaning                                                              |
| ----------------------------- | -------------------------------------------------------------------- |
| `exceeded_max_step`           | Spot price reduction too large; check `max_step` field               |
| `can_lower_every_600_seconds` | Must wait before lowering spot price again; check `time_to_lowering` |

### Python Error Handling Example

```python
from clore_client import CloreClient, CloreAPIError
import time

client = CloreClient(api_key="YOUR_API_KEY")

def create_order_with_retry(server_id, image, max_retries=3):
    for attempt in range(max_retries):
        try:
            return client.create_order(
                server_id=server_id,
                image=image,
                order_type="on-demand",
                currency="bitcoin",
                ports={"22": "tcp"},
                ssh_password="SecurePass123",
            )
        except CloreAPIError as e:
            if e.code == 5:  # Rate limit
                print(f"Rate limited. Waiting 5s... (attempt {attempt+1}/{max_retries})")
                time.sleep(5)
            elif e.code == 3:  # Bad API key
                print("Invalid API key! Check your CLORE_API_KEY.")
                raise
            elif e.code == 2:  # Bad input
                print(f"Invalid request: {e}")
                raise
            else:
                print(f"API error: {e}. Retrying in 3s...")
                time.sleep(3)
    
    raise RuntimeError(f"Failed after {max_retries} attempts")
```

### JavaScript Error Handling Example

```javascript
async function createOrderWithRetry(client, serverConfig, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await client.createOrder(serverConfig);
    } catch (err) {
      const code = parseInt(err.message.match(/error (\d+)/)?.[1]);
      
      if (code === 5) {
        console.log(`Rate limited. Waiting 5s... (attempt ${attempt + 1}/${maxRetries})`);
        await new Promise(r => setTimeout(r, 5000));
      } else if (code === 3) {
        throw new Error('Invalid API key');
      } else {
        console.log(`API error: ${err.message}. Retrying in 3s...`);
        await new Promise(r => setTimeout(r, 3000));
      }
    }
  }
  throw new Error(`Failed after ${maxRetries} attempts`);
}
```

***

## Available Docker Images

Clore.ai provides pre-built images optimized for GPU workloads:

| Image                         | Description               |
| ----------------------------- | ------------------------- |
| `cloreai/ubuntu20.04-jupyter` | Ubuntu 20.04 + JupyterLab |
| `cloreai/ubuntu22.04-jupyter` | Ubuntu 22.04 + JupyterLab |

You can also use any public Docker Hub image. For GPU access, use CUDA-enabled images, e.g.:

```
nvidia/cuda:12.1.0-devel-ubuntu22.04
```

***

## Port Forwarding

When creating an order, specify ports to expose:

```json
{
  "ports": {
    "22": "tcp",
    "8888": "http",
    "6006": "http"
  }
}
```

* **`"tcp"`** — Direct TCP port forwarding (for SSH, custom servers)
* **`"http"`** — HTTPS proxy (for Jupyter, web UIs). Only one HTTP port allowed per `http_port` field.

After order creation, connection details appear in `my_orders`:

```json
{
  "pub_cluster": ["n1.c1.clorecloud.net", "n2.c1.clorecloud.net"],
  "tcp_ports": ["22:10000"],
  "http_port": "8888"
}
```

Connect via SSH:

```bash
ssh root@n1.c1.clorecloud.net -p 10000
```

Access Jupyter via browser: `https://n1.c1.clorecloud.net` (uses the `http_port`)
