Clore.ai proporciona un REST API que permite acceso programático completo al mercado de GPU: listar servidores, crear pedidos, monitorear despliegues y cancelar alquileres.
Nota: No hay un binario CLI oficial en este momento. Toda la automatización se realiza directamente a través de la REST API usando herramientas como curl, Python o Node.js.
URL base:https://api.clore.ai/v1
Formato de respuesta: JSON. Cada respuesta incluye un código campo que indica el estado.
Los pedidos Spot son más baratos pero pueden ser sobrepujados. Usted fija su precio por día:
Comprobar el estado del pedido
Los pedidos activos incluyen pub_cluster (nombres de host) y tcp_ports para acceso SSH:
Conectarse por SSH a su servidor alquilado:
Cancelar un pedido
Opcionalmente informe de un problema con el servidor:
SDK de Python
Un wrapper ligero que usa la requests biblioteca. Instálela con:
Clase CloreClient
Ejemplo completo funcional
Ejemplo en Node.js
Usando la nativa fetch API (Node.js 18+):
Flujos de trabajo comunes
Encontrar la RTX 4090 más barata y alquilarla
Monitorear mis pedidos
Alquiler automático cuando el precio baje por debajo de X
WebSocket
La API REST de Clore.ai actualmente no expone un endpoint WebSocket. Para monitoreo en tiempo real, use sondeos con un intervalo razonable (vea los límites de tasa abajo).
Límites de tasa
Endpoint
Límite
La mayoría de los endpoints
1 solicitud/segundo
create_order
1 solicitud/5 segundos
set_spot_price (reducción de precio)
Una vez cada 600 segundos
Respuesta de límite de tasa (código 5):
Mejores prácticas:
Agregue time.sleep(1) entre llamadas consecutivas a la API
Para create_order, espere al menos 5 segundos entre solicitudes
Use intervalos de sondeo de 60+ segundos para bucles de monitoreo
Caché los datos del marketplace localmente si necesita consultarlos con frecuencia
Ayudante de Python:
Manejo de errores
Cada respuesta de la API incluye un código campo. Un valor de 0 significa éxito.
Códigos de error
Código
Significado
Acción
0
Éxito
—
1
Error de base de datos
Reintentar después de un retraso
2
Datos de entrada inválidos
Verifique el cuerpo/los parámetros de su solicitud
3
Token de API inválido
Verifique su clave API en el panel
4
Endpoint inválido
Verifique la URL del endpoint
5
Límite de tasa excedido (1 req/sec)
Agregue retrasos entre solicitudes
6
Error de la aplicación (vea error campo)
Lea el error campo para más detalles
Sub-errores del Código 6
error valor
Significado
exceeded_max_step
Reducción del precio spot demasiado grande; revise max_step campo
can_lower_every_600_seconds
Debe esperar antes de bajar el precio spot nuevamente; verifique time_to_lowering
Ejemplo de manejo de errores en Python
Ejemplo de manejo de errores en JavaScript
Imágenes Docker disponibles
Clore.ai proporciona imágenes preconstruidas optimizadas para cargas de trabajo GPU:
Imagen
Descripción
cloreai/ubuntu20.04-jupyter
Ubuntu 20.04 + JupyterLab
cloreai/ubuntu22.04-jupyter
Ubuntu 22.04 + JupyterLab
También puede usar cualquier imagen pública de Docker Hub. Para acceso a GPU, use imágenes con soporte CUDA, por ejemplo:
Reenvío de puertos
Al crear una orden, especifique los puertos a exponer:
"tcp" — Reenvío directo de puerto TCP (para SSH, servidores personalizados)
"http" — Proxy HTTPS (para Jupyter, interfaces web). Solo se permite un puerto HTTP por http_port campo.
Después de la creación de la orden, los detalles de conexión aparecen en my_orders:
Conéctese vía SSH:
Acceda a Jupyter vía navegador: https://n1.c1.clorecloud.net (usa el http_port)
curl -XPOST \
-H 'auth: YOUR_API_KEY' \
-H 'Content-type: application/json' \
-d '{
"id": 38,
"issue": "La GPU se estaba sobrecalentando y reduciendo el rendimiento"
}' \
'https://api.clore.ai/v1/cancel_order'
pip install requests
import requests
import time
class CloreClient:
"""SDK sencillo en Python para la REST API de Clore.ai."""
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:
"""Obtener todos los servidores disponibles en el marketplace."""
data = self._get("marketplace")
return data["servers"]
def get_server_details(self, server_id: int) -> dict:
"""Obtener información del mercado spot para un servidor específico."""
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:
"""
Crear un pedido on-demand o spot.
Args:
server_id: ID del servidor a alquilar
image: Imagen Docker (p. ej. 'cloreai/ubuntu20.04-jupyter')
order_type: 'on-demand' o 'spot'
currency: 'bitcoin' (por defecto)
spotprice: Requerido para pedidos spot — precio por día en BTC
ports: Reenvío de puertos, p. ej. {"22": "tcp", "8888": "http"}
ssh_password: Contraseña SSH (solo caracteres alfanuméricos)
ssh_key: Clave pública SSH
jupyter_token: Token del notebook Jupyter
env: Diccionario de variables de entorno
command: Comando de shell a ejecutar después de iniciar el contenedor
"""
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:
"""Obtener sus pedidos activos (y opcionalmente los completados)."""
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:
"""Cancelar un pedido. Opcionalmente informar un problema."""
body = {"id": order_id}
if issue:
body["issue"] = issue
return self._post("cancel_order", body)
def get_wallets(self) -> list:
"""Obtener sus wallets y saldos."""
data = self._get("wallets")
return data["wallets"]
def get_my_servers(self) -> list:
"""Obtener servidores que usted está proveyendo al marketplace."""
data = self._get("my_servers")
return data["servers"]
class CloreAPIError(Exception):
"""Elevada cuando la API de Clore devuelve un código distinto de cero."""
ERROR_CODES = {
0: "Normal",
1: "Error de base de datos",
2: "Datos de entrada inválidos",
3: "Token de API inválido",
4: "Endpoint inválido",
5: "Límite de tasa excedido (1 req/seg)",
6: "Error (ver campo error)",
}
def __init__(self, response: dict):
self.code = response.get("code")
self.error = response.get("error", "")
message = self.ERROR_CODES.get(self.code, f"Código desconocido {self.code}")
if self.error:
message = f"{message}: {self.error}"
super().__init__(f"Error de la API de Clore {self.code}: {message}")
import os
from clore_client import CloreClient, CloreAPIError
# Inicializar cliente
client = CloreClient(api_key=os.environ["CLORE_API_KEY"])
# 1. Explorar el marketplace
servers = client.list_servers()
print(f"Se encontraron {len(servers)} servidores en el marketplace")
# 2. Filtrar servidores RTX 4090 disponibles
rtx4090_servers = [
s for s in servers
if "4090" in s["specs"].get("gpu", "") and not s["rented"]
]
if not rtx4090_servers:
print("No se encontraron servidores RTX 4090 disponibles")
exit(1)
# 3. Elegir el más barato
cheapest = min(rtx4090_servers, key=lambda s: s["price"]["on_demand"]["bitcoin"])
print(f"RTX 4090 más barato: ID de servidor {cheapest['id']}, "
f"precio {cheapest['price']['on_demand']['bitcoin']:.8f} BTC/día")
# 4. Crear un pedido
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("¡Pedido creado con éxito!")
except CloreAPIError as e:
print(f"Error al crear el pedido: {e}")
exit(1)
# 5. Verifique sus pedidos
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"Pedido {order['id']}: servidor {order['si']}")
if cluster and tcp:
ssh_port = tcp[0].split(":")[1]
print(f" SSH: ssh root@{cluster[0]} -p {ssh_port}")
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()
# Filtro: RTX 4090 disponibles
candidates = [
s for s in servers
if "4090" in s["specs"].get("gpu", "")
and not s["rented"]
]
if not candidates:
raise RuntimeError("No se encontraron servidores RTX 4090 disponibles")
# Ordenar por precio BTC on-demand
candidates.sort(key=lambda s: s["price"]["on_demand"]["bitcoin"])
best = candidates[0]
price_btc = best["price"]["on_demand"]["bitcoin"]
print(f"Alquilando servidor {best['id']}: {best['specs']['gpu']} @ {price_btc:.8f} BTC/día")
client.create_order(
server_id=best["id"],
image=image,
order_type="on-demand",
currency="bitcoin",
ports={"22": "tcp"},
ssh_password=ssh_password,
)
print("¡Listo! Verifique sus pedidos para detalles de conexión 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):
"""Consultar pedidos e imprimir actualizaciones de estado."""
print(f"Monitoreando pedidos (consulta cada {poll_interval_seconds}s). Ctrl+C para detener.\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)} pedido(s) activo(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" Pedido {order['id']}: servidor {order['si']}"
f" | gastado {spend:.8f} BTC{ssh_info}")
if not active:
print(" No hay pedidos activos.")
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,
):
"""
Vigile el marketplace y alquile automáticamente una GPU cuando el precio baje por debajo del umbral.
Args:
gpu_model: Nombre de la GPU a buscar (insensible a mayúsculas)
max_price_btc: Precio máximo aceptable por día en BTC
image: Imagen Docker a desplegar
ssh_password: Contraseña SSH para el contenedor
check_interval_seconds: Con qué frecuencia verificar (¡respete los límites de tasa!)
"""
print(f"Buscando {gpu_model} a ≤ {max_price_btc:.8f} BTC/día...")
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"🎯 ¡Coincidencia encontrada! Servidor {server['id']}: {gpu} @ {price:.8f} BTC/día")
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, # Fijar este precio
)
print(f"✅ ¡Pedido creado para el servidor {server['id']}!")
return server["id"]
except Exception as e:
print(f"No se pudo crear la orden: {e}. Reintentará...")
print(f"Aún no hay coincidencia. Comprobando de nuevo en {check_interval_seconds}s...")
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):
"""Envuelva una llamada a la API con seguridad de límite de tasa."""
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: # Límite de tasa
print(f"Limitado por tasa. Esperando 5s... (intento {attempt+1}/{max_retries})")
time.sleep(5)
elif e.code == 3: # Clave API inválida
print("¡Clave API inválida! Verifique su CLORE_API_KEY.")
raise
elif e.code == 2: # Entrada inválida
print(f"Solicitud inválida: {e}")
raise
else:
print(f"Error de la API: {e}. Reintentando en 3s...")
time.sleep(3)
raise RuntimeError(f"Falló después de {max_retries} intentos")
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(`Limitado por tasa. Esperando 5s... (intento ${attempt + 1}/${maxRetries})`);
await new Promise(r => setTimeout(r, 5000));
} else if (code === 3) {
throw new Error('Clave API inválida');
} else {
console.log(`Error de la API: ${err.message}. Reintentando en 3s...`);
await new Promise(r => setTimeout(r, 3000));
}
}
}
throw new Error(`Falló después de ${maxRetries} intentos`);
}