# Guía de CLI y SDK

## Resumen

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.

***

## Autenticación

Genere su clave API desde el [Panel de control de Clore.ai](https://clore.ai):

1. Inicie sesión en su cuenta
2. Navegue a **API** sección en la configuración
3. Genere y copie su clave API

**Formato del encabezado:**

```
auth: SU_CLAVE_API
```

> ⚠️ **Importante:** El encabezado auth es `auth`, **no** `Authorization: Bearer`. Usar el formato incorrecto devolverá el código `3` (Token de API inválido).

**Ejemplo:**

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

***

## Inicio rápido

### Listar servidores del marketplace

Explore todos los servidores GPU disponibles:

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

**Respuesta:**

```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
}
```

***

### Obtener sus pedidos

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

Incluir pedidos completados/expirados:

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

***

### Crear un pedido (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'
```

**Respuesta:**

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

***

### Crear un pedido Spot

Los pedidos Spot son más baratos pero pueden ser sobrepujados. Usted fija su precio por día:

```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'
```

***

### Comprobar el estado del pedido

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

Los pedidos activos incluyen `pub_cluster` (nombres de host) y `tcp_ports` para acceso SSH:

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

Conectarse por SSH a su servidor alquilado:

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

***

### Cancelar un pedido

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

Opcionalmente informe de un problema con el servidor:

```bash
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'
```

***

## SDK de Python

Un wrapper ligero que usa la `requests` biblioteca. Instálela con:

```bash
pip install requests
```

### Clase CloreClient

```python
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}")
```

### Ejemplo completo funcional

```python
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}")
```

***

## Ejemplo en Node.js

Usando la nativa `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);
  }
}

// Ejemplo de uso
const client = new CloreClient(process.env.CLORE_API_KEY);

async function main() {
  // Listar marketplace
  const servers = await client.listServers();
  console.log(`El marketplace tiene ${servers.length} servidores`);

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

  // Obtener pedidos actuales
  const orders = await client.getOrders();
  const activeOrders = orders.filter(o => !o.expired);
  console.log(`Pedidos activos: ${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(`Pedido ${order.id} → ssh root@${host} -p ${sshPort}`);
    }
  }
}

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

***

## Flujos de trabajo comunes

### Encontrar la RTX 4090 más barata y alquilarla

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

***

### Monitorear mis pedidos

```python
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()
```

***

### Alquiler automático cuando el precio baje por debajo de 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,
):
    """
    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)
```

***

## 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):**

```json
{ "code": 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:**

```python
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
```

***

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

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

### Ejemplo de manejo de errores en JavaScript

```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(`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`);
}
```

***

## 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:

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

***

## Reenvío de puertos

Al crear una orden, especifique los puertos a exponer:

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

* **`"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`:

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

Conéctese vía SSH:

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

Acceda a Jupyter vía navegador: `https://n1.c1.clorecloud.net` (usa el `http_port`)
