Clore.ai предоставляет REST API который обеспечивает полный программный доступ к рынку GPU — просмотр серверов, создание заказов, мониторинг развертываний и отмену аренды.
Примечание: В настоящий момент официального CLI-бинарника нет. Вся автоматизация осуществляется напрямую через REST API с помощью таких инструментов, как curl, Python или Node.js.
Базовый URL:https://api.clore.ai/v1
Формат ответа: JSON. Каждый ответ содержит code поле, указывающее статус.
Спотовые заказы дешевле, но могут быть перебиты. Вы указываете вашу цену в день:
Проверить статус заказа
Активные заказы содержат pub_cluster (имена хостов) и tcp_ports для доступа по SSH:
Подключение по SSH к арендованному серверу:
Отменить заказ
По желанию сообщите о проблеме с сервером:
Python SDK
Легкий обертка, использующая requests библиотеку. Установите её с помощью:
Класс CloreClient
Полный рабочий пример
Пример Node.js
Использование нативного fetch API (Node.js 18+):
Распространенные рабочие сценарии
Найти самый дешевый RTX 4090 и арендовать его
Мониторинг моих заказов
Авто-аренда при падении цены ниже X
WebSocket
REST API Clore.ai в настоящее время не предоставляет конечную точку WebSocket. Для мониторинга в реальном времени используйте опрос с разумным интервалом (см. лимиты скорости ниже).
Лимиты скорости
Конечная точка
Лимит
Большинство конечных точек
1 запрос/секунда
create_order
1 запрос/5 секунд
set_spot_price (снижение цены)
Один раз каждые 600 секунд
Ответ при превышении лимита (код 5):
Рекомендуемые практики:
Добавьте time.sleep(1) между последовательными вызовами API
Для create_order, подождите по крайней мере 5 секунд между запросами
Используйте интервалы опроса 60+ секунд для циклов мониторинга
Кэшируйте данные маркетплейса локально, если вам нужно часто их запрашивать
Помощник на Python:
Обработка ошибок
Каждый ответ API включает поле code поле. Значение 0 означает успех.
Коды ошибок
Код
Значение
Действие
0
Успех
—
1
Ошибка базы данных
Повторите через некоторое время
2
Неверные входные данные
Проверьте тело запроса/параметры
3
Неверный API токен
Проверьте ваш API-ключ в панели управления
4
Неверная конечная точка
Проверьте URL конечной точки
5
Превышен лимит запросов (1 запрос/сек)
Добавьте задержки между запросами
6
Ошибка приложения (см. ошибка поле)
Прочитайте ошибка поле для подробностей
Подошибки кода 6
ошибка значение
Значение
exceeded_max_step
Снижение спотовой цены слишком велико; проверьте max_step поле
can_lower_every_600_seconds
Нужно подождать перед следующим снижением спотовой цены; проверьте time_to_lowering
Пример обработки ошибок в Python
Пример обработки ошибок в JavaScript
Доступные Docker-образы
Clore.ai предоставляет предварительно собранные образы, оптимизированные для задач на GPU:
Образ
Описание
cloreai/ubuntu20.04-jupyter
Ubuntu 20.04 + JupyterLab
cloreai/ubuntu22.04-jupyter
Ubuntu 22.04 + JupyterLab
Вы также можете использовать любой публичный образ с Docker Hub. Для доступа к GPU используйте образы с поддержкой CUDA, например:
Проброс портов
При создании заказа укажите порты для экспорта:
"tcp" — Прямой проброс TCP-порта (для SSH, пользовательских серверов)
"http" — HTTPS-прокси (для Jupyter, веб-интерфейсов). Разрешён только один HTTP-порт на http_port поле.
После создания заказа данные для подключения появятся в my_orders:
Подключиться через SSH:
Доступ к Jupyter через браузер: https://n1.c1.clorecloud.net (использует http_port)
import requests
import time
class CloreClient:
"""Простая Python SDK для 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:
"""Получить все доступные серверы на рынке."""
data = self._get("marketplace")
return data["servers"]
def get_server_details(self, server_id: int) -> dict:
"""Получить информацию о спотовом рынке для конкретного сервера."""
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:
"""
Создать on-demand или спотовый заказ.
Аргументы:
server_id: ID сервера для аренды
image: Docker-образ (например, 'cloreai/ubuntu20.04-jupyter')
order_type: 'on-demand' или 'spot'
currency: 'bitcoin' (по умолчанию)
spotprice: Обязательно для спотовых заказов — цена в день в BTC
ports: Проброс портов, например {"22": "tcp", "8888": "http"}
ssh_password: Пароль SSH (только буквенно-цифровые символы)
ssh_key: Публичный SSH-ключ
jupyter_token: Токен для Jupyter notebook
env: Словарь переменных окружения
command: Команда оболочки для выполнения после запуска контейнера
"""
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:
"""Получить ваши активные (и опционально завершенные) заказы."""
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:
"""Отменить заказ. По желанию сообщить о проблеме."""
body = {"id": order_id}
if issue:
body["issue"] = issue
return self._post("cancel_order", body)
def get_wallets(self) -> list:
"""Получить ваши кошельки и балансы."""
data = self._get("wallets")
return data["wallets"]
def get_my_servers(self) -> list:
"""Получить серверы, которые вы предоставляете на рынок."""
data = self._get("my_servers")
return data["servers"]
class CloreAPIError(Exception):
"""Возникает, когда Clore API возвращает код, отличный от нуля."""
ERROR_CODES = {
0: "Нормально",
1: "Ошибка базы данных",
2: "Неверные входные данные",
3: "Неверный API-токен",
4: "Неверный endpoint",
5: "Превышен лимит запросов (1 запрос/сек)",
6: "Ошибка (см. поле error)",
}
def __init__(self, response: dict):
self.code = response.get("code")
self.error = response.get("error", "")
message = self.ERROR_CODES.get(self.code, f"Неизвестный код {self.code}")
if self.error:
message = f"{message}: {self.error}"
super().__init__(f"Ошибка Clore API {self.code}: {message}")
import os
from clore_client import CloreClient, CloreAPIError
# Инициализация клиента
client = CloreClient(api_key=os.environ["CLORE_API_KEY"])
# 1. Просмотреть рынок
servers = client.list_servers()
print(f"Найдено {len(servers)} серверов на рынке")
# 2. Отфильтровать доступные серверы RTX 4090
rtx4090_servers = [
s for s in servers
if "4090" in s["specs"].get("gpu", "") and not s["rented"]
]
if not rtx4090_servers:
print("Не найдено доступных серверов RTX 4090")
exit(1)
# 3. Выбрать самый дешевый
cheapest = min(rtx4090_servers, key=lambda s: s["price"]["on_demand"]["bitcoin"])
print(f"Самый дешевый RTX 4090: ID сервера {cheapest['id']}, "
f"цена {cheapest['price']['on_demand']['bitcoin']:.8f} BTC/день")
# 4. Создать заказ
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("Заказ успешно создан!")
except CloreAPIError as e:
print(f"Не удалось создать заказ: {e}")
exit(1)
# 5. Проверить ваши заказы
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['id']}: сервер {order['si']}")
if cluster and tcp:
ssh_port = tcp[0].split(":")[1]
print(f" SSH: ssh root@{cluster[0]} -p {ssh_port}")
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);
}
}
// Пример использования
const client = new CloreClient(process.env.CLORE_API_KEY);
async function main() {
// Список на рынке
const servers = await client.listServers();
console.log(`На рынке ${servers.length} серверов`);
// Найти доступные серверы RTX 4090
const available = servers.filter(
s => s.specs.gpu.includes('4090') && !s.rented
);
console.log(`Доступных серверов RTX 4090: ${available.length}`);
// Получить текущие заказы
const orders = await client.getOrders();
const activeOrders = orders.filter(o => !o.expired);
console.log(`Активных заказов: ${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.id} → ssh root@${host} -p ${sshPort}`);
}
}
}
main().catch(console.error);
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()
# Фильтр: доступный RTX 4090
candidates = [
s for s in servers
if "4090" in s["specs"].get("gpu", "")
and not s["rented"]
]
if not candidates:
raise RuntimeError("Не найдено доступных серверов RTX 4090")
# Сортировать по цене on-demand в BTC
candidates.sort(key=lambda s: s["price"]["on_demand"]["bitcoin"])
best = candidates[0]
price_btc = best["price"]["on_demand"]["bitcoin"]
print(f"Аренда сервера {best['id']}: {best['specs']['gpu']} @ {price_btc:.8f} BTC/день")
client.create_order(
server_id=best["id"],
image=image,
order_type="on-demand",
currency="bitcoin",
ports={"22": "tcp"},
ssh_password=ssh_password,
)
print("Готово! Проверьте ваши заказы для деталей подключения по SSH.")
return best["id"]
rent_cheapest_rtx4090()
import time
from clore_client import CloreClient
client = CloreClient(api_key="YOUR_API_KEY")
def monitor_orders(poll_interval_seconds=60):
"""Опрос заказов и вывод обновлений статуса."""
print(f"Мониторинг заказов (опрос каждые {poll_interval_seconds}s). Ctrl+C чтобы остановить.\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)} активных заказ(а/ов) ---")
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['id']}: сервер {order['si']}"
f" | потрачено {spend:.8f} BTC{ssh_info}")
if not active:
print(" Нет активных заказов.")
print()
time.sleep(poll_interval_seconds)
monitor_orders()
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,
):
"""
Следить за рынком и автоматически арендовать GPU, когда цена опускается ниже порога.
Аргументы:
gpu_model: Название GPU для поиска (без учета регистра)
max_price_btc: Максимально допустимая цена в день в BTC
image: Docker-образ для развертывания
ssh_password: Пароль SSH для контейнера
check_interval_seconds: Как часто проверять (уважайте лимиты запросов!)
"""
print(f"Отслеживание {gpu_model} при цене ≤ {max_price_btc:.8f} BTC/день...")
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"🎯 Найдена подходящая машина! Сервер {server['id']}: {gpu} @ {price:.8f} BTC/день")
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, # Зафиксировать эту цену
)
print(f"✅ Заказ создан для сервера {server['id']}!")
return server["id"]
except Exception as e:
print(f"Не удалось создать заказ: {e}. Попробую снова...")
print(f"Совпадения пока нет. Проверим снова через {check_interval_seconds}с...")
time.sleep(check_interval_seconds)
auto_rent_on_price_drop(gpu_model="4090", max_price_btc=0.00012)
{ "code": 5 }
import time
def safe_api_call(fn, *args, delay=1.1, **kwargs):
"""Обёртка для вызова API с защитой от превышения лимитов."""
result = fn(*args, **kwargs)
time.sleep(delay)
return result
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"Превышен лимит. Ждём 5с... (попытка {attempt+1}/{max_retries})")
time.sleep(5)
elif e.code == 3: # Bad API key
print("Неверный API ключ! Проверьте ваш CLORE_API_KEY.")
raise
elif e.code == 2: # Bad input
print(f"Неверный запрос: {e}")
raise
else:
print(f"Ошибка API: {e}. Повтор через 3с...")
time.sleep(3)
raise RuntimeError(f"Не удалось после {max_retries} попыток")
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(`Превышен лимит. Ждём 5с... (попытка ${attempt + 1}/${maxRetries})`);
await new Promise(r => setTimeout(r, 5000));
} else if (code === 3) {
throw new Error('Неверный API ключ');
} else {
console.log(`Ошибка API: ${err.message}. Повтор через 3с...`);
await new Promise(r => setTimeout(r, 3000));
}
}
}
throw new Error(`Не удалось после ${maxRetries} попыток`);
}