# API saldo token dompet: aset ERC-20 dan riwayat transfer

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

Bangun halaman aset dompet dengan API data dompet blockchain: gunakan [Token Balances API](https://blockvectra.com/en/data/balances/) untuk kepemilikan ERC-20 non-nol dan [Token Transfers API](https://blockvectra.com/en/data/transfers/) untuk riwayat dompet. Pengembang dan AI Agent menggunakan permintaan terautentikasi yang sama. Sebelum melakukan kueri, baca [GET /v1/status](https://api.blockvectra.com/v1/data) dan periksa `data_features` serta `data_status` chain yang dipilih; cakupan saldo berbeda untuk setiap chain. Parameter permintaan dan skema respons tersedia di [referensi Data API](https://docs.blockvectra.com/en/api/data/).

## Tugas yang dapat Anda selesaikan dengan panduan ini

* [Baca saldo token dompet](#request-1-address-balances) dengan API key dan gunakan paginasi untuk kepemilikan ERC-20 non-nol.
* [Baca riwayat transfer dompet](#request-2-address-transfers) dalam jendela blok tetap dan ikuti kursor untuk alamat yang dipilih.
* [Lengkapi metadata token](#request-3-token-metadata-and-tokensbatch) untuk menampilkan nama dan simbol bersama saldo bilangan bulat mentah, dengan tetap mempertahankan bidang yang tidak tersedia.

## Tiga jenis data yang dibutuhkan halaman aset dompet

Halaman aset dompet dapat menampilkan saldo token ERC-20, riwayat transfer token, dan metadata token suatu alamat. Data API menyediakan endpoint untuk masing-masing:

* **Saldo**: `GET /{chain}/addresses/{address}/balances` mengembalikan saldo ERC-20 non-nol alamat tersebut, diurutkan berdasarkan alamat `token` secara menaik, dengan `symbol` dan `decimals` token disertakan jika tersedia. Alamat tanpa saldo mengembalikan `200` dengan `data: []`.
* **Transfer**: `GET /{chain}/addresses/{address}/transfers` mengembalikan transfer token yang melibatkan alamat tersebut dalam jendela blok yang wajib ditentukan, diurutkan berdasarkan `(block_number, log_index)` secara menurun.
* **Metadata token**: `GET /{chain}/tokens/{token}` membaca nama, simbol, decimals, dan total suplai satu token berdasarkan alamat kontrak; `POST /{chain}/tokens:batch` membaca metadata yang sama untuk hingga 100 alamat dalam satu permintaan.

Ketiganya menggunakan `https://api.blockvectra.com/v1/data` sebagai URL dasar dan header permintaan `x-api-key`, dengan `robinhood_mainnet` sebagai chain contoh. Masing-masing termasuk dalam kemampuan `balances`, `transfers`, dan `token_metadata`; untuk chain yang menyediakan setiap kemampuan, lihat halaman [Chain yang Didukung](https://docs.blockvectra.com/en/chains/). Pada chain tanpa kemampuan tersebut, endpoint mengembalikan `422 no_coverage`.

## Permintaan 1: saldo alamat

Endpoint ini memerlukan lebih sedikit parameter, sehingga cocok sebagai permintaan pertama untuk suatu halaman:

* `{chain}` (parameter path, wajib): pengenal chain, yaitu nilai `chain` dari entri dalam `GET /chains` (misalnya `robinhood_mainnet`). Pencocokan harus persis dan peka huruf besar-kecil; alias dan Chain ID numerik tidak diterima.
* `{address}` (parameter path, wajib): alamat 20-byte; awalan `0x` opsional dan huruf besar maupun kecil diterima.
* `limit` (parameter kueri, opsional): ukuran halaman. Nilai bawaan 50; nilai di atas 500 dibatasi menjadi 500; `0` atau nilai non-bilangan bulat mengembalikan `400 bad_request`.
* `cursor` (parameter kueri, opsional): `next_cursor` dari respons sebelumnya, dikirim kembali tanpa perubahan untuk mengambil halaman berikutnya. Kursor hanya berlaku untuk chain, endpoint, dan parameter kueri yang menerbitkannya; penggunaan di tempat lain mengembalikan `400 bad_request`.

Paginasi menggunakan keyset: `next_cursor` hanya muncul jika ada halaman berikutnya. Pada halaman terakhir, kunci tersebut sama sekali tidak ada, bukan `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"])
```


Struktur respons adalah `AddressBalanceListEnvelope`, yang berisi `data` dan `meta`. Setiap item `data` adalah `AddressBalance`:

| Bidang     | Tipe                  | Deskripsi                                                                                                                                                |
| ---------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`    | `string` (alamat)     | Alamat kontrak token; bentuk kanonisnya adalah `0x` diikuti 40 digit heksadesimal huruf kecil.                                                           |
| `balance`  | `string` (desimal)    | Saldo bilangan bulat mentah, yang dapat melebihi `2^53`, dikembalikan sebagai string desimal biasa — bukan angka JSON, notasi ilmiah, atau heksadesimal. |
| `symbol`   | `string` atau `null`  | Simbol token, atau `null` jika tidak tersedia.                                                                                                           |
| `decimals` | `integer` atau `null` | Decimals token, `0`–`255`, atau `null` jika tidak tersedia.                                                                                              |

## Permintaan 2: transfer alamat

Endpoint transfer memerlukan jendela blok eksplisit: `from_block` dan `to_block` keduanya wajib dan harus memenuhi `from_block <= to_block`. Endpoint ini menerima beberapa parameter tambahan:

* `standard` (parameter kueri, wajib): `erc20` atau `erc721`. Kueri dengan cakupan alamat tidak mencakup `erc1155`; mengirimkannya mengembalikan `422 no_coverage`.
* `direction` (parameter kueri, opsional): `in`, `out`, atau `any`; nilai bawaan `any` dan menyaring berdasarkan arah relatif terhadap alamat.
* `token` (parameter kueri, opsional): membatasi hasil pada satu kontrak token.
* `clamp` (parameter kueri, opsional): hanya string literal `true` yang mengaktifkannya; nilai lainnya dianggap `false`.

Batas jendela dan finalitas: `to_block` eksplisit di atas `as_of_block` mengembalikan `409 not_indexed_yet` kecuali `clamp=true` memangkasnya ke `as_of_block`; jendela yang lebih lebar dari batas chain (`limits.max_window_blocks` dari `GET /chains`) mengembalikan `409 window_too_large` kecuali `clamp=true` memangkas dari ujung yang lebih lama (menaikkan `from_block` dan mempertahankan `to_block`). Jika `from_block` sendiri sudah melewati `as_of_block`, respons tetap `409` tanpa pengecualian meskipun `clamp=true`. Saat jendela dipangkas atau hanya tercakup sebagian, `meta.coverage` respons adalah `"partial"`; selain itu `"full"`.

Dalam catatan transfer, item ERC-20 menambahkan `amount`; item ERC-721 menambahkan `token_id`. Keduanya mencakup `token`, `standard`, `from`, `to`, `block_number`, `block_timestamp`, `tx_hash`, `tx_index`, dan `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"])
```


## Menelusuri semua transfer dengan paginasi

`next_cursor` endpoint transfer alamat bersifat optimistis: hanya muncul ketika halaman mengembalikan tepat `limit` baris, sehingga halaman dapat memiliki `next_cursor` meskipun ternyata merupakan halaman terakhir. Jangan berhenti ketika halaman kosong; ikuti `next_cursor` sampai kunci tersebut tidak ada.

Kode berikut mengambil setiap transfer dalam jendela:

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


## Permintaan 3: metadata token dan tokens:batch

Baca satu token dengan `GET /{chain}/tokens/{token}`; path hanya menerima `{chain}` dan `{token}`, tanpa paginasi. Struktur respons adalah `TokenEnvelope`, dan `data` adalah `Token`:

| Bidang                | Tipe                  | Deskripsi                                                                                                                                                                  |
| --------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`             | `string` (alamat)     | Alamat kontrak token.                                                                                                                                                      |
| `standard`            | `string`              | `erc20`, `erc721`, atau `unknown`.                                                                                                                                         |
| `name`                | `string` atau `null`  | Nama token, atau `null` jika tidak tersedia.                                                                                                                               |
| `symbol`              | `string` atau `null`  | Simbol token, atau `null` jika tidak tersedia.                                                                                                                             |
| `decimals`            | `integer` atau `null` | Decimals token, `0`–`255`, atau `null` jika tidak tersedia.                                                                                                                |
| `total_supply`        | `string` atau `null`  | Total suplai mentah; API tidak menerapkan penskalaan `decimals`. `null` jika tidak tersedia.                                                                               |
| `first_seen_block`    | `integer` (int64)     | Tinggi blok tempat token pertama kali terlihat.                                                                                                                            |
| `metadata_updated_at` | `string` (timestamp)  | Waktu UTC saat metadata terakhir diperbarui.                                                                                                                               |
| `metadata_block`      | `integer` (int64)     | Tinggi blok saat metadata dibaca.                                                                                                                                          |
| `metadata_status`     | `string`              | `ok`, `partial`, atau `unavailable`.                                                                                                                                       |
| `metadata_issues`     | `object`              | Catatan masalah per bidang dengan kunci `name`, `symbol`, `decimals`, `total_supply`, dan nilai `reverted`, `no_data`, `invalid_encoding`, atau `temporarily_unavailable`. |

`{token}` yang bukan alamat 20-byte yang valid mengembalikan `400 bad_request`; `{token}` yang tidak dikenal mengembalikan `404 not_found`; `{chain}` yang tidak dikenal mengembalikan `404 unknown_chain`.

Endpoint saldo sudah menyertakan `symbol` dan `decimals` jika tersedia, tetapi keduanya dapat bernilai `null`. Untuk melengkapi nama dan decimals setiap token dalam dompet, gunakan `POST /{chain}/tokens:batch`:

* Body permintaan adalah `{"addresses": [...]}` dengan paling banyak 100 alamat per permintaan; lebih dari 100 entri, atau entri yang bukan alamat 20-byte yang valid, mengembalikan `400 bad_request` (gagal pada alamat tidak valid pertama yang ditemui).
* Alamat yang tidak ditemukan tidak memicu error; alamat tersebut tercantum dalam `data.missing`, sedangkan `data.tokens` hanya berisi token yang metadatanya ditemukan.
* Alamat berulang dideduplikasi dalam `tokens` dan `missing`, masing-masing mengikuti urutan kemunculan pertama dalam permintaan.

**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"])
```


## Menskalakan jumlah berdasarkan decimals

Bidang saldo `balance` dan bidang transfer ERC-20 `amount` adalah bilangan bulat mentah yang ditampilkan sebagai string desimal (`UInt256String`); `total_supply` token juga merupakan bilangan bulat on-chain mentah tanpa penskalaan `decimals`. Untuk menampilkan jumlah yang mudah dibaca manusia, bagi berdasarkan `decimals` token tersebut.

* `decimals` berasal dari `symbol`/`decimals` item saldo itu sendiri, atau dari `GET /{chain}/tokens/{token}` dan `POST /{chain}/tokens:batch`; nilainya dapat berupa `null`.
* Nilai ini dapat melebihi `2^53`, jadi jangan menghitung menggunakan angka JSON: gunakan `BigInt` dalam TypeScript dan `Decimal` dalam Python, dengan mem-parsing string desimal apa adanya untuk menghindari hilangnya presisi.

**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"])
```


## Kebaruan data

Setiap respons sukses dengan cakupan chain menyertakan `meta`:

* `as_of_block`: blok terbaru chain yang sepenuhnya sudah ditulis. Endpoint dengan cakupan blok menyajikan data hingga tinggi ini.
* `safe_block`: penanda tag blok konsensus `safe` node (`null` jika belum diketahui). Tidak pernah di bawah `finalized_block`, dan tidak memangkas, menolak, atau menunda respons.
* `finalized_block`: penanda tag blok konsensus `finalized` node (`null` jika belum diketahui). Tidak memangkas, menolak, atau menunda respons; klien menentukan keamanan yang dibutuhkan dari penanda tersebut (seperti status konfirmasi).
* `coverage`: `"full"` atau `"partial"`. Transfer alamat dan endpoint serupa melaporkan `"partial"` ketika `clamp` mempersempit jendela yang disajikan, atau ketika jendela dimulai sebelum blok pertama yang diindeks pada chain.
* `refreshed_at`: waktu data di balik respons terakhir diperbarui (UTC). Dapat bernilai `null`: `null` berarti waktu pembaruan data tidak diketahui dan data harus dianggap usang; endpoint berbasis blok selalu mengembalikan nilai.
* Respons juga mengulang `chain`, `chain_slug`, dan `chain_external_id`.

Pola yang umum: baca `meta.as_of_block` dari respons pertama mana pun untuk membaca hingga blok terbaru yang diindeks, dan periksa `meta.safe_block` / `meta.finalized_block` jika ingin menampilkan status terkonfirmasi.

## Estimasi CU untuk satu pemuatan halaman

Setiap metode ditagih berdasarkan bobot CU-nya, yang dibaca dari API paket platform:

**Bobot CU per panggilan**

| Metode | CU per panggilan |
| --- | --- |
| `data.address_balances` | 25 |
| `data.address_transfers` | 25 |
| `data.tokens_batch` | 10 |

**Satu kali muat halaman (perkiraan)**

1 permintaan saldo + 3 halaman transfer + 1 permintaan `tokens:batch`, total 5 panggilan, sekitar 110 CU. Penggunaan sebenarnya tergantung pada jumlah halaman dan token.

Untuk keputusan penagihan dan respons error yang tidak ditagih, lihat [aturan penagihan](https://docs.blockvectra.com/en/guides/billing-rules/). Jika yang Anda butuhkan bukan riwayat transfer terindeks melainkan log dari blok terbaru, baca [Data node terbaru vs riwayat terindeks](https://docs.blockvectra.com/en/guides/logs-vs-transfers/) sebelum memutuskan untuk beralih ke `eth_getLogs`.

## Langkah selanjutnya

* [Jelajahi direktori dataset](https://blockvectra.com/en/data/) untuk melihat setiap dataset yang diindeks BlockVectra.
* [Lihat paket gratis dan harga](https://blockvectra.com/en/pricing/#free) untuk memeriksa apa yang termasuk dalam akun Anda.
* [Masuk ke konsol](https://console.blockvectra.com/login/?next=%2Fkeys%2F) untuk membuat API key.
