# API de saldos de tokens de billeteras: activos ERC-20 e historial de transferencias

> Source: https://docs.blockvectra.com/es/guides/wallet-assets/

Cree una página de activos de billetera con la API de datos de billeteras blockchain: utilice la [API de saldos de tokens](https://blockvectra.com/es/data/balances/) para las tenencias ERC-20 distintas de cero y la [API de transferencias de tokens](https://blockvectra.com/es/data/transfers/) para el historial de la billetera. Los desarrolladores y agentes de IA utilizan las mismas solicitudes autenticadas. Antes de consultar, lea [GET /v1/status](https://api.blockvectra.com/v1/data) y compruebe `data_features` y `data_status` de la cadena seleccionada; la cobertura de saldos varía según la cadena. Los parámetros de solicitud y los esquemas de respuesta están en la [referencia de la Data API](https://docs.blockvectra.com/en/api/data/).

## Tareas que esta guía le ayuda a completar

* [Leer los saldos de tokens de una billetera](#request-1-address-balances) con una clave y paginar las tenencias ERC-20 distintas de cero.
* [Leer el historial de transferencias de una billetera](#request-2-address-transfers) dentro de una ventana de bloques fija y seguir los cursores para la dirección seleccionada.
* [Completar los metadatos de tokens](#request-3-token-metadata-and-tokensbatch) para mostrar nombres y símbolos junto a los saldos enteros brutos, conservando los campos faltantes.

## Los tres tipos de datos que necesita una página de activos de billetera

Una página de activos de billetera puede mostrar los saldos de tokens ERC-20 de una dirección, su historial de transferencias y los metadatos de tokens. La Data API ofrece un endpoint para cada uno:

* **Saldos**: `GET /{chain}/addresses/{address}/balances` devuelve los saldos ERC-20 distintos de cero de la dirección, ordenados por dirección `token` de forma ascendente, con `symbol` y `decimals` del token cuando están disponibles. Una dirección sin saldos devuelve `200` con `data: []`.
* **Transferencias**: `GET /{chain}/addresses/{address}/transfers` devuelve las transferencias de tokens que involucran a la dirección dentro de una ventana de bloques obligatoria, ordenadas por `(block_number, log_index)` de forma descendente.
* **Metadatos de tokens**: `GET /{chain}/tokens/{token}` lee el nombre, símbolo, decimales y suministro total de un token por dirección de contrato; `POST /{chain}/tokens:batch` lee los mismos metadatos de hasta 100 direcciones en una solicitud.

Los tres utilizan `https://api.blockvectra.com/v1/data` como URL base y el encabezado de solicitud `x-api-key`, con `robinhood_mainnet` como cadena de ejemplo. Pertenecen a las capacidades `balances`, `transfers` y `token_metadata`, respectivamente; para conocer las cadenas que ofrecen cada capacidad, consulte la página de [Cadenas compatibles](https://docs.blockvectra.com/es/chains/). En una cadena sin esa capacidad, el endpoint devuelve `422 no_coverage`.

## Solicitud 1: saldos de una dirección

Este endpoint requiere menos parámetros, por lo que resulta adecuado como primera solicitud de una página:

* `{chain}` (parámetro de ruta, obligatorio): identificador de la cadena, el valor `chain` de una entrada de `GET /chains` (por ejemplo `robinhood_mainnet`). La coincidencia es exacta y distingue mayúsculas y minúsculas; no se aceptan alias ni IDs de cadena numéricos.
* `{address}` (parámetro de ruta, obligatorio): dirección de 20 bytes; el prefijo `0x` es opcional y se aceptan mayúsculas y minúsculas.
* `limit` (parámetro de consulta, opcional): tamaño de página. El valor predeterminado es 50; los valores superiores a 500 se limitan a 500; `0` o un valor no entero devuelve `400 bad_request`.
* `cursor` (parámetro de consulta, opcional): el `next_cursor` de la respuesta anterior, devuelto sin cambios para obtener la siguiente página. Un cursor solo es válido para la cadena, el endpoint y los parámetros de consulta que lo generaron; reutilizarlo en otro contexto devuelve `400 bad_request`.

La paginación utiliza claves: `next_cursor` aparece solo cuando hay otra página. En la última página, la clave está completamente ausente, nunca es `null`.

**cURL**

```bash
export BLOCKVECTRA_API_KEY=rgw_your_api_key

curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/balances?limit=50" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"
```


  **TypeScript**

```ts
const address = "0x1111111111111111111111111111111111111111";
const url = new URL(
  `https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/balances`,
);
url.searchParams.set("limit", "50");

const res = await fetch(url, {
  headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! },
});
const balanceBody = await res.json();
console.log(balanceBody.data, balanceBody.meta);

// npx tsx example.mts
```


  **Python**

```python
import os
import requests

address = "0x1111111111111111111111111111111111111111"
res = requests.get(
    f"https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/{address}/balances",
    params={"limit": 50},
    headers={"x-api-key": os.environ["BLOCKVECTRA_API_KEY"]},
)
res.raise_for_status()
balance_body = res.json()
print(balance_body["data"], balance_body["meta"])
```


La estructura de respuesta es `AddressBalanceListEnvelope`, que contiene `data` y `meta`. Cada elemento de `data` es un `AddressBalance`:

| Campo      | Tipo                 | Descripción                                                                                                                                           |
| ---------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`    | `string` (dirección) | Dirección del contrato del token; la forma canónica es `0x` seguido de 40 dígitos hexadecimales en minúsculas.                                        |
| `balance`  | `string` (decimal)   | Saldo entero bruto, que puede superar `2^53`, devuelto como una cadena decimal simple; nunca como un número JSON, notación científica ni hexadecimal. |
| `symbol`   | `string` o `null`    | Símbolo del token, o `null` si no está disponible.                                                                                                    |
| `decimals` | `integer` o `null`   | Decimales del token, `0`–`255`, o `null` si no están disponibles.                                                                                     |

## Solicitud 2: transferencias de una dirección

El endpoint de transferencias requiere una ventana de bloques explícita: tanto `from_block` como `to_block` son obligatorios y deben cumplir `from_block <= to_block`. Acepta algunos parámetros adicionales:

* `standard` (parámetro de consulta, obligatorio): `erc20` o `erc721`. Las consultas por dirección no cubren `erc1155`; pasarlo devuelve `422 no_coverage`.
* `direction` (parámetro de consulta, opcional): `in`, `out` o `any`; el valor predeterminado es `any` y filtra por dirección de la transferencia respecto de la dirección consultada.
* `token` (parámetro de consulta, opcional): restringe los resultados a un contrato de token.
* `clamp` (parámetro de consulta, opcional): solo la cadena literal `true` lo activa; cualquier otro valor se trata como `false`.

Límites de ventana y finalidad: un `to_block` explícito superior a `as_of_block` devuelve `409 not_indexed_yet`, salvo que `clamp=true` lo reduzca a `as_of_block`; una ventana más amplia que el límite de la cadena (`limits.max_window_blocks` de `GET /chains`) devuelve `409 window_too_large`, salvo que `clamp=true` recorte el extremo más antiguo (aumentando `from_block` y manteniendo fijo `to_block`). Si el propio `from_block` ya supera `as_of_block`, sigue devolviendo un `409` obligatorio incluso con `clamp=true`. Cuando la ventana se recorta o solo tiene cobertura parcial, `meta.coverage` de la respuesta es `"partial"`; en caso contrario, es `"full"`.

En los registros de transferencias, los elementos ERC-20 añaden `amount`; los elementos ERC-721 añaden `token_id`. Ambos incluyen `token`, `standard`, `from`, `to`, `block_number`, `block_timestamp`, `tx_hash`, `tx_index` y `log_index`.

**cURL**

```bash
export BLOCKVECTRA_API_KEY=rgw_your_api_key

# 1) Read as_of_block from the balances response.
AS_OF_BLOCK=$(curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/balances" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" | grep -o '"as_of_block":[0-9]*' | cut -d: -f2)

# 2) clamp=true truncates a too-wide window, or a to_block above as_of_block, instead of returning 409.
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/0x1111111111111111111111111111111111111111/transfers?standard=erc20&from_block=0&to_block=$AS_OF_BLOCK&direction=any&clamp=true" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"
```


  **TypeScript**

```ts
const address = "0x1111111111111111111111111111111111111111";

// 1) Read as_of_block from any previous response's meta.
const head = await fetch(
  `https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/balances`,
  { headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! } },
).then((r) => r.json());

// 2) Use as_of_block as the transfer window's upper bound.
const url = new URL(
  `https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/transfers`,
);
url.searchParams.set("standard", "erc20");
url.searchParams.set("from_block", "0");
url.searchParams.set("to_block", String(head.meta.as_of_block));
url.searchParams.set("direction", "any");
url.searchParams.set("clamp", "true");

const res = await fetch(url, {
  headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! },
});
const body = await res.json();
console.log(body.data, body.meta);

// npx tsx example.mts
```


  **Python**

```python
import os
import requests

address = "0x1111111111111111111111111111111111111111"
headers = {"x-api-key": os.environ["BLOCKVECTRA_API_KEY"]}

# 1) Read as_of_block from any previous response's meta.
head = requests.get(
    f"https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/{address}/balances",
    headers=headers,
).json()

# 2) Use as_of_block as the transfer window's upper bound.
res = requests.get(
    f"https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/{address}/transfers",
    params={
        "standard": "erc20",
        "from_block": 0,
        "to_block": head["meta"]["as_of_block"],
        "direction": "any",
        "clamp": "true",
    },
    headers=headers,
)
res.raise_for_status()
body = res.json()
print(body["data"], body["meta"])
```


## Paginar todas las transferencias

El `next_cursor` del endpoint de transferencias por dirección es optimista: aparece solo cuando la página devuelve exactamente `limit` filas, por lo que una página puede incluir un `next_cursor` y aun así resultar ser la última. No se detenga cuando una página esté vacía; siga `next_cursor` hasta que la clave esté ausente.

El código siguiente obtiene todas las transferencias de la ventana:

**TypeScript**

```ts
const address = "0x1111111111111111111111111111111111111111";
const head = await fetch(
  `https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/balances`,
  { headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! } },
).then((r) => r.json());
const asOfBlock = head.meta.as_of_block;
const transfers: unknown[] = [];
let cursor: string | undefined;

do {
  const url = new URL(
    `https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/${address}/transfers`,
  );
  url.searchParams.set("standard", "erc20");
  url.searchParams.set("from_block", "0");
  url.searchParams.set("to_block", String(asOfBlock));
  url.searchParams.set("limit", "500");
  // clamp truncates from the older end
  url.searchParams.set("clamp", "true");
  if (cursor) url.searchParams.set("cursor", cursor);

  const res = await fetch(url, {
    headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! },
  });
  const page = await res.json();
  transfers.push(...page.data);
  cursor = page.next_cursor; // absent on the last page
} while (cursor);
```


  **Python**

```python
import os
import requests

address = "0x1111111111111111111111111111111111111111"
head = requests.get(
    f"https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/{address}/balances",
    headers={"x-api-key": os.environ["BLOCKVECTRA_API_KEY"]},
).json()
as_of_block = head["meta"]["as_of_block"]
transfers = []
cursor = None

while True:
    params = {
        "standard": "erc20",
        "from_block": 0,
        "to_block": as_of_block,
        "limit": 500,
        # clamp truncates from the older end
        "clamp": "true",
    }
    if cursor:
        params["cursor"] = cursor
    res = requests.get(
        f"https://api.blockvectra.com/v1/data/robinhood_mainnet/addresses/{address}/transfers",
        params=params,
        headers={"x-api-key": os.environ["BLOCKVECTRA_API_KEY"]},
    )
    res.raise_for_status()
    page = res.json()
    transfers.extend(page["data"])
    cursor = page.get("next_cursor")  # absent on the last page
    if not cursor:
        break
```


## Solicitud 3: metadatos de tokens y tokens:batch

Lea un token individual con `GET /{chain}/tokens/{token}`; la ruta acepta solo `{chain}` y `{token}`, sin paginación. La estructura de respuesta es `TokenEnvelope` y `data` es un `Token`:

| Campo                 | Tipo                       | Descripción                                                                                                                                                               |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`             | `string` (dirección)       | Dirección del contrato del token.                                                                                                                                         |
| `standard`            | `string`                   | `erc20`, `erc721` o `unknown`.                                                                                                                                            |
| `name`                | `string` o `null`          | Nombre del token, o `null` si no está disponible.                                                                                                                         |
| `symbol`              | `string` o `null`          | Símbolo del token, o `null` si no está disponible.                                                                                                                        |
| `decimals`            | `integer` o `null`         | Decimales del token, `0`–`255`, o `null` si no están disponibles.                                                                                                         |
| `total_supply`        | `string` o `null`          | Suministro total bruto; la API no aplica el ajuste por `decimals`. Es `null` si no está disponible.                                                                       |
| `first_seen_block`    | `integer` (int64)          | Altura del bloque en el que se detectó el token por primera vez.                                                                                                          |
| `metadata_updated_at` | `string` (marca de tiempo) | Hora UTC de la última actualización de los metadatos.                                                                                                                     |
| `metadata_block`      | `integer` (int64)          | Altura del bloque en el que se leyeron los metadatos.                                                                                                                     |
| `metadata_status`     | `string`                   | `ok`, `partial` o `unavailable`.                                                                                                                                          |
| `metadata_issues`     | `object`                   | Registros de problemas por campo con claves `name`, `symbol`, `decimals`, `total_supply` y valores `reverted`, `no_data`, `invalid_encoding` o `temporarily_unavailable`. |

Un `{token}` que no sea una dirección válida de 20 bytes devuelve `400 bad_request`; un `{token}` desconocido devuelve `404 not_found`; un `{chain}` desconocido devuelve `404 unknown_chain`.

El endpoint de saldos ya incluye `symbol` y `decimals` cuando están disponibles, pero ambos pueden ser `null`. Para completar el nombre y los decimales de cada token de una billetera, utilice `POST /{chain}/tokens:batch`:

* El body de la solicitud es `{"addresses": [...]}` con un máximo de 100 direcciones por solicitud; más de 100 entradas o una entrada que no sea una dirección válida de 20 bytes devuelve `400 bad_request` (falla en la primera dirección inválida que encuentra al recorrerlas).
* Las direcciones no encontradas no generan un error; se enumeran en `data.missing`, mientras que `data.tokens` contiene solo los tokens cuyos metadatos se encontraron.
* Las direcciones duplicadas se deduplican tanto en `tokens` como en `missing`, cada uno siguiendo el orden de la primera aparición en la solicitud.

**cURL**

```bash
export BLOCKVECTRA_API_KEY=rgw_your_api_key

# Single token
curl -s "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens/0x1111111111111111111111111111111111111111" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"

# Batch: up to 100 addresses per request
curl -s -X POST "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens:batch" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"addresses":["0x1111111111111111111111111111111111111111","0x2222222222222222222222222222222222222222"]}'
```


  **TypeScript**

```ts
// Single token
const single = await fetch(
  "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens/0x1111111111111111111111111111111111111111",
  { headers: { "x-api-key": process.env.BLOCKVECTRA_API_KEY! } },
).then((r) => r.json());
console.log(single.data);

// Batch: group by 100 addresses to enrich the tokens from the balances response
const BATCH_SIZE = 100;
const addresses = balanceBody.data.map((item: { token: string }) => item.token);
const tokens = new Map<string, unknown>();
const missing: string[] = [];

for (let i = 0; i < addresses.length; i += BATCH_SIZE) {
  const res = await fetch("https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens:batch", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.BLOCKVECTRA_API_KEY!,
    },
    body: JSON.stringify({ addresses: addresses.slice(i, i + BATCH_SIZE) }),
  });
  const body = await res.json();
  for (const token of body.data.tokens) tokens.set(token.address, token);
  missing.push(...body.data.missing);
}

// npx tsx example.mts
```


  **Python**

```python
import os
import requests

headers = {"x-api-key": os.environ["BLOCKVECTRA_API_KEY"]}

# Single token
single = requests.get(
    "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens/0x1111111111111111111111111111111111111111",
    headers=headers,
).json()
print(single["data"])

# Batch: group by 100 addresses to enrich the tokens from the balances response
BATCH_SIZE = 100
addresses = [item["token"] for item in balance_body["data"]]
tokens = {}
missing = []

for i in range(0, len(addresses), BATCH_SIZE):
    res = requests.post(
        "https://api.blockvectra.com/v1/data/robinhood_mainnet/tokens:batch",
        json={"addresses": addresses[i : i + BATCH_SIZE]},
        headers={**headers, "Content-Type": "application/json"},
    )
    res.raise_for_status()
    body = res.json()
    for token in body["data"]["tokens"]:
        tokens[token["address"]] = token
    missing.extend(body["data"]["missing"])
```


## Ajustar los importes según los decimales

El campo de saldo `balance` y el campo de transferencia ERC-20 `amount` son enteros brutos representados como cadenas decimales (`UInt256String`); el `total_supply` de un token también es un entero bruto on-chain sin ajuste por `decimals`. Para mostrar una cantidad legible por humanos, divida según los `decimals` de ese token.

* `decimals` procede de los propios `symbol`/`decimals` del elemento de saldo o de `GET /{chain}/tokens/{token}` y `POST /{chain}/tokens:batch`; puede ser `null`.
* Estos valores pueden superar `2^53`, así que no realice los cálculos con un número JSON: utilice `BigInt` en TypeScript y `Decimal` en Python, analizando la cadena decimal tal como está para evitar pérdidas de precisión.

**TypeScript**

```ts
function toDisplayAmount(raw: string, decimals: number | null): string {
  if (decimals === null) return raw; // no decimals metadata: keep the raw integer
  const value = BigInt(raw);
  const base = 10n ** BigInt(decimals);
  const whole = value / base;
  const fraction = (value % base)
    .toString()
    .padStart(decimals, "0")
    .replace(/0+$/, "");
  return fraction ? `${whole}.${fraction}` : whole.toString();
}

// balance.balance is a raw decimal string; decimals comes from the same item or tokens:batch.
const display = toDisplayAmount(balance.balance, balance.decimals);
```


  **Python**

```python
from decimal import Decimal


def to_display_amount(raw: str, decimals: int | None) -> str:
    if decimals is None:
        return raw  # no decimals metadata: keep the raw integer
    value = Decimal(raw)  # parse the decimal string exactly
    return format(value.scaleb(-decimals).normalize(), "f")


# balance["balance"] is a raw decimal string; decimals comes from the same item or tokens:batch.
display = to_display_amount(balance["balance"], balance["decimals"])
```


## Actualidad de los datos

Cada respuesta exitosa de una cadena incluye `meta`:

* `as_of_block`: el bloque más reciente de la cadena completamente escrito. Los endpoints por bloques sirven datos hasta esta altura.
* `safe_block`: un marcador que indica la etiqueta de bloque de consenso `safe` del nodo (`null` mientras se desconoce). Nunca es inferior a `finalized_block` y no recorta, rechaza ni retrasa respuestas.
* `finalized_block`: un marcador que indica la etiqueta de bloque de consenso `finalized` del nodo (`null` mientras se desconoce). No recorta, rechaza ni retrasa respuestas; los clientes deciden qué seguridad necesitan del marcador (como el estado de confirmación).
* `coverage`: `"full"` o `"partial"`. Las transferencias por dirección y endpoints similares informan `"partial"` cuando `clamp` redujo la ventana servida o cuando la ventana comienza antes del primer bloque indexado de la cadena.
* `refreshed_at`: cuándo se actualizaron por última vez los datos de la respuesta (UTC). Puede ser `null`: `null` significa que se desconoce la hora de actualización de los datos y deben tratarse como desactualizados; los endpoints basados en bloques siempre devuelven un valor.
* También repite `chain`, `chain_slug` y `chain_external_id`.

Un patrón habitual: lea `meta.as_of_block` de cualquier primera respuesta para consultar hasta el bloque indexado más reciente y compruebe `meta.safe_block` / `meta.finalized_block` si desea mostrar el estado confirmado.

## Estimación de CU para una carga de página

Cada método se factura según su peso CU, leído de la API de planes de la plataforma:

**Peso en CU por llamada**

| Método | CU por llamada |
| --- | --- |
| `data.address_balances` | 25 |
| `data.address_transfers` | 25 |
| `data.tokens_batch` | 10 |

**Una carga de página (estimada)**

1 solicitud de saldos + 3 páginas de transferencias + 1 solicitud(es) `tokens:batch`, 5 llamadas en total, aprox. 110 CU. El uso real depende del número de páginas y tokens.

Para conocer los criterios de facturación y las respuestas de error no facturadas, consulte las [reglas de facturación](https://docs.blockvectra.com/en/guides/billing-rules/). Si necesita logs de los bloques más recientes en lugar de historial de transferencias indexadas, lea primero [Datos recientes del nodo frente a historial indexado](https://docs.blockvectra.com/es/guides/logs-vs-transfers/) antes de decidir si cambia a `eth_getLogs`.

## Próximos pasos

* [Explore el directorio de conjuntos de datos](https://blockvectra.com/es/data/) para ver todos los conjuntos de datos indexados por BlockVectra.
* [Consulte el plan gratuito y los precios](https://blockvectra.com/es/pricing/#free) para comprobar lo que incluye su cuenta.
* [Inicie sesión en la consola](https://console.blockvectra.com/login/?next=%2Fkeys%2F) para crear una API key.
