# 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`)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.clore.ai/developers/cli-sdk.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
