Saltar al contenido principal

Acciones tokenizadas (xStocks)

Las acciones tokenizadas de Skipo (NVDASPV, TSLASPV, AMDSPV, GLDSPV…) son activos rebasing. La cantidad liquidada nunca se mueve; las acciones corporativas —reinversión de dividendos, splits, splits inversos— se expresan subiendo un multiplicador:

acciones subyacentes = cantidad liquidada × multiplicador
precio por acción = tasa / multiplicador

Las dos representaciones describen el mismo dinero: cantidad × tasa da el mismo importe en la moneda de cotización en ambos casos. El rebase afecta solo a la pata base — un mercado xStock es <ACCIÓN>-CLP o <ACCIÓN>-USDT, así que la otra pata es fiat o una stablecoin.

assetFormat​

Los endpoints de órdenes, fills, depósitos, retiros, saldos, precio indicativo y cotizaciones aceptan un parámetro assetFormat:

ValorQué devuelve
rebased (por defecto)Importes en términos de la acción subyacente — lo que un tenedor considera "sus acciones".
baseImportes en términos de los tokens SPV liquidados — lo que se movió en cadena y en el libro contable.

Dónde se acepta:

RecursoassetFormat
Órdenes y fills — GET /v2/orders, /v2/orders/{id}, /v2/orders/{id}/fills, /v2/fillssí — por defecto rebased
Retiros — GET /v2/withdrawalssí — por defecto rebased
Depósitos — GET /v2/depositssí — por defecto rebased
Saldos — GET /v2/balancessí — por defecto rebased
Precio indicativo — GET /v2/markets/{market}/pricesí — por defecto rebased
Cotizaciones — POST /v2/quotessí, en el cuerpo — por defecto rebased
Libro contable — GET /v2/ledgerno — siempre liquidado (por qué)
Historial de saldos — GET /v2/balances/{assetSymbol}/historyno — siempre liquidado (por qué)
Webhooksno — siempre rebased (por qué)

La lista autoritativa de parámetros de cada endpoint está en la referencia de endpoints, que se genera desde la propia especificación OpenAPI. Recuerda que la API rechaza los parámetros que no conoce: mandar assetFormat a un endpoint que no lo acepta es un 400, no algo que se ignore en silencio.

GET /v2/fills?market=NVDASPV-CLP # rebased (por defecto)
GET /v2/fills?market=NVDASPV-CLP&assetFormat=base # cifras liquidadas
GET /v2/deposits?assetSymbol=COPXX # rebased (por defecto)
GET /v2/withdrawals?assetFormat=base # cifras liquidadas
GET /v2/balances # rebased (por defecto)
GET /v2/balances?assetFormat=base # cantidades SPV liquidadas
GET /v2/markets/NVDASPV-CLP/price?side=BUY # precio por acción (por defecto)
En cotizaciones el parámetro va en el CUERPO

POST /v2/quotes lo lleva en el cuerpo de la petición, no en la query. No es un capricho: assetFormat es la unidad del amount que mandas, y un valor y su unidad no deberían viajar por caminos distintos.

{ "baseAsset": "NVDASPV", "quoteAsset": "CLP", "amountAsset": "NVDASPV",
"side": "BUY", "amount": "10", "assetFormat": "base" }

En las dos rutas de cotización —la confirmable y el precio indicativo— gobierna las dos direcciones: cómo se interpreta el amount que envías y en qué representación vuelven baseAmount y rate. El market no cambia en ninguna de las dos: nombra un par de instrumentos, no una cantidad.

El valor por defecto es rebased, en todos ellos

Es un cambio deliberado respecto al comportamiento anterior, que devolvía base. Un inversor piensa en acciones, no en tokens SPV, y es lo que hacen las plataformas comparables (el rebase_multiplier de Kraken tiene el mismo valor por defecto). Si tu integración depende de las cifras liquidadas, pide assetFormat=base explícitamente.

El valor por defecto es el mismo en todos los recursos que lo aceptan: no hay endpoints que sigan devolviendo base de forma implícita.

En los demás activos no cambia nada: el multiplicador es 1 y ambos modos son idénticos.

Los dos nombres: NVDASPV y NVDAX​

Una acción tokenizada tiene dos códigos para el mismo activo:

CódigoQué esDónde aparece
NVDASPVEl token SPV liquidado — lo que se movió en cadena y en el libro contableassetSymbol en la data de referencia, base.asset, el libro, el id del market
NVDAXLa acción subyacente — lo que un tenedor considera que tienebaseAsset en órdenes y fills, assetSymbol en saldos, y toda la UI

La data de referencia publica los dos, así que no hace falta hardcodear el mapeo:

// GET /v2/assets/NVDASPV
{ "assetSymbol": "NVDASPV", "rebasedSymbol": "NVDAX", "assetClass": "STOCK" }

// GET /v2/markets/NVDASPV-CLP
{ "id": "NVDASPV-CLP", "baseAsset": "NVDASPV", "rebasedBaseAsset": "NVDAX", "quoteAsset": "CLP" }

En un activo que no rebasa los dos campos traen el mismo valor, así que una UI puede leer rebasedSymbol sin ramificar. Que sean iguales no afirma que el activo no rebase: TSLASPV está hoy en factor 1 y es un STOCK. La clase la da assetClass.

Las dos escrituras se aceptan en la entrada​

Donde un endpoint pide un activo, cualquiera de las dos escrituras resuelve. NVDAX selecciona el mismo activo que NVDASPV:

GET /v2/assets/NVDAX # devuelve el activo NVDASPV
GET /v2/markets/NVDAX-CLP # devuelve el market NVDASPV-CLP
GET /v2/fills?assetSymbol=NVDAX # los mismos fills
GET /v2/balances/NVDAX/history # la misma serie
POST /v2/quotes {"baseAsset":"NVDAX", ...} # el mismo market

Dos cosas que conviene tener claras:

La respuesta siempre usa la escritura canónica. Pedir NVDAX-CLP devuelve id: "NVDASPV-CLP". Un id de market nombra un par de instrumentos, no una cantidad, y nunca rebasa.

La escritura no define las unidades. Solo assetFormat lo hace. Mandar amountAsset: "NVDAX" con assetFormat: "base" cotiza tokens liquidados, porque pediste base. Un símbolo es un nombre; la representación es ese campo.

POST /v2/withdrawals es la excepción: solo acepta el código liquidado

Es el único sitio donde la escritura rebasada se rechaza, y no es un descuido.

Ese cuerpo no lleva assetFormat, así que la representación de amount es implícita: son tokens liquidados. Si aceptáramos NVDAX traduciendo el símbolo sin convertir el monto, retiraríamos una cantidad distinta de la que pediste — con NFLXSPV, cuyo factor es 10, pedir 1 acción sacaría 10.

Y la trampa es fácil de pisar: GET /v2/balances responde rebasado por defecto y te entrega assetSymbol: "NFLXX". Mandar ese valor acá es el paso natural.

Así que se rechaza con un 400 que dice las unidades y la conversión, no solo la escritura:

{
"code": "validation_error",
"detail": "This endpoint takes the settled asset code and SETTLED amounts. 'NFLXX' is the rebased ticker for 'NFLXSPV' (1 NFLXSPV = 10 NFLXX). Resend with assetSymbol 'NFLXSPV' and 'amount' converted to settled units — divide by 10.",
"extensions": { "assetSymbol": "NFLXSPV", "rebasedSymbol": "NFLXX", "multiplier": "10" }
}

Una instrucción firmada se ejecuta como fue escrita: el monto es tuyo, no nuestro para reinterpretar.

El campo multiplier​

Órdenes, fills, depósitos, retiros, saldos y cotizaciones publican el factor aplicado:

{
"id": "1096473",
"baseAsset": "NVDASPV",
"baseAmount": "10.000658218334353",
"rate": "18499.8",
"multiplier": "1.0000658218334353"
}

La relación fundamental, en una línea:

importe base × multiplier = importe rebasado

Con él puedes reconstruir la otra representación sin pedir de nuevo:

  • base → rebased: baseAmount × multiplier, rate ÷ multiplier
  • rebased → base: baseAmount ÷ multiplier, rate × multiplier

Esto es lo que permite conciliar un importe de la API contra un saldo en cadena.

En los payloads de webhook no hace falta ni multiplicar: todos los eventos —retiros, depósitos, órdenes y fills— llegan con las dos representaciones ya calculadas —el nivel superior rebasado y un bloque base con las cifras liquidadas— más el multiplier que las relaciona. Ver Webhooks.

Y ya no hace falta multiplicar en ninguna superficie: saldos, órdenes, fills, retiros y depósitos llegan todos con el nivel superior en la representación que pediste y un bloque base con las cifras liquidadas. Conciliar una tenencia o una ejecución contra el libro contable o contra un webhook no exige una segunda lectura con otros parámetros.

En una orden el bloque lleva las dos patas y el par lleno/pendiente; en un fill, las dos patas más los acumulados liquidados de su orden:

// GET /v2/orders (assetFormat=rebased, el valor por defecto)
{
"market": "NVDASPV-CLP",
"baseAsset": "NVDAX",
"baseAmount": "0.3214865",
"rate": "207290.19029004",
"multiplier": "1.000103090792305",
"assetFormat": "rebased",
"base": {
"baseAsset": "NVDASPV",
"quoteAsset": "CLP",
"baseAmount": "0.32145337",
"filledBaseAmount": "0.32145337",
"quoteAmount": "66641",
"filledQuoteAmount": "66641",
"rate": "207311.56"
}
}
El bloque base se LEE, no se deriva — y tú tampoco deberías derivarlo

Las fórmulas de arriba sirven para entender la relación, pero no para reconstruir el bloque base a partir del nivel superior. Dos razones, y las dos muerden:

El rebase es asimétrico. La pata base multiplica, rate divide, la pata de cotización no se toca. Una división uniforme corrompe la cotización.

Y el nivel superior ya viene truncado a la precisión del activo, así que dividir de vuelta re-fabrica una cola que no es lo que liquidó. Con cifras reales: el baseAmount liquidado de esa orden es 0.32145337, y dividir el 0.3214865 publicado de vuelta da 0.32145336 — un dígito de valor real, perdido en silencio.

Por eso publicamos las dos: la fila sin escalar la tenemos nosotros. Lee base, no lo calcules.

{
"assetSymbol": "NVDAX",
"balance": "125.57296048",
"multiplier": "1.0009180758490996",
"assetFormat": "rebased",
"base": { "asset": "NVDASPV", "balance": "125.45778073" }
}

base.asset viene poblado en toda superficie y en cualquier assetFormat, retiros y depósitos incluidos. Antes ahí podía venir null en una lectura rebasada, porque el código SPV ya no viajaba en esa respuesta; ahora el ticker rebasado se resuelve de vuelta al código liquidado, así que el dato está siempre.

Solo queda null si esa resolución falla, lo que significa que el activo no está en el registro — no que el dato fuera inobtenible. Nunca es el ticker subyacente: un código parecido pero equivocado es peor que un hueco evidente cuando estás conciliando contra un webhook.

Ojo con aplicar las fórmulas de arriba a una orden completa: solo valen para la pata base. El importe de la pata de cotización (quoteAmount) no se toca, y por eso el rate se divide — ver una orden rebasa solo una de sus dos patas.

El multiplicador es puntual en el tiempo​

Es la parte que más importa y la más fácil de equivocar:

El factor que publicamos es el que estaba vigente en el momento de ese movimiento —ese fill, ese depósito, ese retiro—, leído de la propia fila. Nunca es el factor de hoy, y nunca se recalcula.

Una acción corporativa cambia el factor hacia adelante; no cambia lo que entregó una operación pasada. Aplicar el multiplicador de hoy a un fill del año pasado reescribe la historia: si un activo hace un split 10:1, todo tu historial pasado aparecería multiplicado por diez.

Consecuencia práctica: dos movimientos del mismo activo pueden traer multiplicadores distintos, y eso es correcto. No caches "el multiplicador de NVDASPV" como si fuera un dato de referencia: es un dato de cada fila.

Es la regla 3 de la guía de la extensión Scaled UI Amount de Solana: un importe histórico debe mostrarse con el multiplicador vigente cuando ocurrió la transacción.

Un saldo o un precio usan el factor de HOY​

La regla de arriba vale para movimientos: un fill, un depósito, un retiro llevan estampado el factor que estaba vigente cuando ocurrieron, y publicarlos con otro reescribiría la historia.

Un saldo, un precio indicativo y una cotización no son historia: describen el estado de ahora mismo. Su factor es, por definición, el vigente hoy — es exactamente lo que el emisor llama el scaled balance: lo que tienes en cadena por el multiplicador actual. Aplicarles un factor histórico sería tan incorrecto como aplicarle a un fill pasado el de hoy.

En una línea: el factor de una fila lo trae la fila; el de un estado actual lo trae el reloj.

Consecuencia práctica, y no es un error: GET /v2/balances y GET /v2/fills pueden publicar multiplicadores distintos para el mismo activo en el mismo instante. El del saldo es el de hoy; el del fill es el del día en que se ejecutó.

multiplier: "1" no significa "no es una acción tokenizada"

Un factor de exactamente 1 es simplemente un factor que todavía no se ha movido — TSLASPV está hoy en 1. No lo uses para clasificar activos. La clase de activo es dato de referencia y se consulta en GET /v2/assets/{assetSymbol}.

El libro contable no acepta assetFormat​

GET /v2/ledger expone deliberadamente los importes liquidados, sin assetFormat.

El libro es el registro de doble entrada de lo que realmente se movió, y hoy sus asientos no llevan el multiplicador vigente en el momento del apunte. Honrar assetFormat obligaría a resolver el factor en vivo, que es exactamente el error de reescritura descrito arriba. Preferimos una cifra correcta y sin escalar a una cifra escalada y potencialmente falsa.

Para ver un fill en términos de acciones, úsalo desde /v2/fills, que sí lleva el factor de su momento.

El historial de saldos tampoco​

GET /v2/balances/{assetSymbol}/history publica cantidades liquidadas y tampoco acepta assetFormat, por la misma razón de fondo que el libro.

Una cifra rebasada necesita el factor que estaba vigente cuando esa cifra era cierta. Esta serie se calcula desde movimientos de libro y cada punto llega solo con su fecha y su saldo: no hay factor por punto, y no se puede recuperar. Honrar assetFormat significaría aplicar el factor de hoy a todo punto pasado — tras un split 10:1, cada punto anterior saltaría diez veces. Es exactamente la reescritura de historia que la regla de arriba prohíbe.

Preferimos una serie correcta sin escalar a una escalada y posiblemente falsa. Es una brecha real y no una preferencia: se cierra cuando cada punto lleve su propio factor.

La petición sí acepta las dos escrituras —NVDAX lee la serie de NVDASPV—, porque eso solo elige qué activo se lee. Y assetSymbol en la respuesta siempre trae el código liquidado, para que la etiqueta coincida con las unidades que estás leyendo.

La precisión de un importe rebasado​

Multiplicar por el factor fabrica decimales. Un saldo liquidado de 8 decimales por un factor de 16 da un producto de 24, y esa cola es aritmética, no información.

Todo importe rebasado se trunca a la precisión con la que Skipo opera ese activo — el amountIncrement que publica GET /v2/assets, documentado como el paso por debajo del cual un importe se trunca. Se trunca hacia abajo, nunca se redondea hacia arriba: en un saldo publicado nunca sobreestima lo que tienes, y en un importe que viaja a ejecutarse nunca vende más de lo que pediste.

El multiplier que va al lado no se trunca: es el factor, y lo necesitas entero para convertir.