# ウォレットのトークン残高 API：ERC-20 資産と送金履歴

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

ブロックチェーンのウォレットデータ API を使ってウォレット資産ページを構築します。残高がゼロでない ERC-20 保有資産には[トークン残高 API](https://blockvectra.com/en/data/balances/)、ウォレットの履歴には[トークン送金 API](https://blockvectra.com/en/data/transfers/)を使用します。開発者と AI エージェントは同じ認証付きリクエストを使用します。クエリを実行する前に [GET /v1/status](https://api.blockvectra.com/v1/data) を取得し、選択したチェーンの `data_features` と `data_status` を確認してください。残高データの対応範囲はチェーンによって異なります。リクエストパラメータとレスポンススキーマは [Data API リファレンス](https://docs.blockvectra.com/en/api/data/)に記載されています。

## このガイドでできること

* キーを使って[ウォレットのトークン残高を取得](#request-1-address-balances)し、残高がゼロでない ERC-20 保有資産をページ分割して取得します。
* 固定したブロック範囲内の[ウォレットの送金履歴を取得](#request-2-address-transfers)し、選択したアドレスのカーソルをたどります。
* [トークンのメタデータを補完](#request-3-token-metadata-and-tokensbatch)して、未取得のフィールドをそのまま保持しながら、生の整数残高とともに名前やシンボルを表示します。

## ウォレット資産ページに必要な 3 種類のデータ

ウォレット資産ページには、アドレスの ERC-20 トークン残高、トークン送金履歴、トークンのメタデータを表示できます。Data API では、それぞれに対応するエンドポイントを提供しています。

* **残高**：`GET /{chain}/addresses/{address}/balances` は、そのアドレスの残高がゼロでない ERC-20 トークンを、`token` アドレスの昇順で返します。トークンの `symbol` と `decimals` は取得できる場合に含まれます。残高がないアドレスには、`data: []` を含む `200` を返します。
* **送金**：`GET /{chain}/addresses/{address}/transfers` は、必須のブロック範囲内でそのアドレスが関わったトークン送金を、`(block_number, log_index)` の降順で返します。
* **トークンのメタデータ**：`GET /{chain}/tokens/{token}` は、コントラクトアドレスを使って 1 つのトークンの名前、シンボル、小数桁数、総供給量を取得します。`POST /{chain}/tokens:batch` は、同じメタデータを 1 回のリクエストで最大 100 アドレス分取得します。

3 つともベース URL に `https://api.blockvectra.com/v1/data`、リクエストヘッダーに `x-api-key` を使用し、チェーンの例として `robinhood_mainnet` を使います。それぞれ `balances`、`transfers`、`token_metadata` の機能に属します。各機能を提供するチェーンについては、[対応チェーン](https://docs.blockvectra.com/en/chains/)ページを参照してください。その機能に対応していないチェーンでは、エンドポイントは `422 no_coverage` を返します。

## リクエスト 1：アドレスの残高

このエンドポイントはパラメータが少ないため、ページで最初に行うリクエストに適しています。

* `{chain}`（パスパラメータ、必須）：チェーン識別子です。`GET /chains` のエントリーの `chain` 値（例：`robinhood_mainnet`）を指定します。完全一致で、大文字と小文字を区別します。別名や数値の Chain ID は受け付けません。
* `{address}`（パスパラメータ、必須）：20 バイトのアドレスです。`0x` プレフィックスは任意で、大文字と小文字のどちらも受け付けます。
* `limit`（クエリパラメータ、任意）：ページサイズです。デフォルトは 50 で、500 を超える値は 500 に切り詰められます。`0` または整数以外の値は `400 bad_request` を返します。
* `cursor`（クエリパラメータ、任意）：前のレスポンスの `next_cursor` を変更せずに渡し、次のページを取得します。カーソルは、それを発行したチェーン、エンドポイント、クエリパラメータに対してのみ有効です。別の条件で再利用すると `400 bad_request` を返します。

キーセット方式のページネーションを使用します。`next_cursor` は次のページがある場合にのみ含まれます。最終ページではキー自体が存在せず、`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"])
```


レスポンスのエンベロープは `AddressBalanceListEnvelope` で、`data` と `meta` を含みます。`data` の各項目は `AddressBalance` です。

| フィールド      | 型                    | 説明                                                                       |
| ---------- | -------------------- | ------------------------------------------------------------------------ |
| `token`    | `string`（アドレス）       | トークンのコントラクトアドレス。正規形は `0x` に小文字の 16 進数 40 桁を続けた形式です。                      |
| `balance`  | `string`（10 進数）      | 生の整数残高。`2^53` を超える場合があり、通常の 10 進数文字列として返されます。JSON 数値、指数表記、16 進数では返されません。 |
| `symbol`   | `string` または `null`  | トークンのシンボル。取得できない場合は `null` です。                                           |
| `decimals` | `integer` または `null` | トークンの小数桁数。`0`–`255` の値で、取得できない場合は `null` です。                             |

## リクエスト 2：アドレスの送金

送金エンドポイントでは、ブロック範囲を明示する必要があります。`from_block` と `to_block` はどちらも必須で、`from_block <= to_block` を満たす必要があります。次のパラメータも使用します。

* `standard`（クエリパラメータ、必須）：`erc20` または `erc721`。アドレス単位のクエリは `erc1155` に対応していません。指定すると `422 no_coverage` を返します。
* `direction`（クエリパラメータ、任意）：`in`、`out`、`any` のいずれか。デフォルトは `any` で、アドレスから見た送金方向でフィルタリングします。
* `token`（クエリパラメータ、任意）：結果を 1 つのトークンコントラクトに限定します。
* `clamp`（クエリパラメータ、任意）：文字列 `true` をそのまま指定した場合にのみ有効になります。それ以外の値は `false` として扱われます。

範囲の境界とファイナリティ：明示した `to_block` が `as_of_block` を超えると `409 not_indexed_yet` を返します。ただし、`clamp=true` の場合は `as_of_block` まで切り詰められます。範囲がチェーンの上限（`GET /chains` の `limits.max_window_blocks`）を超えると `409 window_too_large` を返します。ただし、`clamp=true` の場合は古い側から切り詰められます（`from_block` を引き上げ、`to_block` は固定します）。`from_block` 自体がすでに `as_of_block` を超えている場合は、`clamp=true` でも必ず `409` を返します。範囲が切り詰められた場合や一部のみが対応範囲内にある場合、レスポンスの `meta.coverage` は `"partial"` になります。それ以外の場合は `"full"` です。

送金レコードでは、ERC-20 の項目には `amount`、ERC-721 の項目には `token_id` が追加されます。どちらも `token`、`standard`、`from`、`to`、`block_number`、`block_timestamp`、`tx_hash`、`tx_index`、`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"])
```


## ページネーションで全送金を取得する

アドレス送金エンドポイントの `next_cursor` は、次のページの存在を確約しません。返された行数がちょうど `limit` の場合にのみ含まれるため、`next_cursor` があるページでも、実際には最終ページである可能性があります。ページが空でも処理を止めず、キーがなくなるまで `next_cursor` をたどってください。

次のコードは、指定範囲内のすべての送金を取得します。

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


## リクエスト 3：トークンのメタデータと tokens:batch

`GET /{chain}/tokens/{token}` で 1 つのトークンを取得します。パスには `{chain}` と `{token}` のみを指定し、ページネーションはありません。レスポンスのエンベロープは `TokenEnvelope` で、`data` は `Token` です。

| フィールド                 | 型                    | 説明                                                                                                                                           |
| --------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`             | `string`（アドレス）       | トークンのコントラクトアドレス。                                                                                                                             |
| `standard`            | `string`             | `erc20`、`erc721`、`unknown` のいずれか。                                                                                                            |
| `name`                | `string` または `null`  | トークン名。取得できない場合は `null` です。                                                                                                                   |
| `symbol`              | `string` または `null`  | トークンのシンボル。取得できない場合は `null` です。                                                                                                               |
| `decimals`            | `integer` または `null` | トークンの小数桁数。`0`–`255` の値で、取得できない場合は `null` です。                                                                                                 |
| `total_supply`        | `string` または `null`  | 生の総供給量。API は `decimals` に応じた換算を行いません。取得できない場合は `null` です。                                                                                    |
| `first_seen_block`    | `integer` (int64)    | トークンが最初に検出されたブロック高。                                                                                                                          |
| `metadata_updated_at` | `string`（タイムスタンプ）    | メタデータが最後に更新された UTC 時刻。                                                                                                                       |
| `metadata_block`      | `integer` (int64)    | メタデータを読み取ったブロック高。                                                                                                                            |
| `metadata_status`     | `string`             | `ok`、`partial`、`unavailable` のいずれか。                                                                                                          |
| `metadata_issues`     | `object`             | `name`、`symbol`、`decimals`、`total_supply` をキーとする、フィールドごとの問題の記録。値は `reverted`、`no_data`、`invalid_encoding`、`temporarily_unavailable` のいずれかです。 |

`{token}` が有効な 20 バイトのアドレスでない場合は `400 bad_request`、未知の `{token}` は `404 not_found`、未知の `{chain}` は `404 unknown_chain` を返します。

残高エンドポイントには、取得できる場合はすでに `symbol` と `decimals` が含まれていますが、どちらも `null` になる可能性があります。ウォレット内のすべてのトークンの名前と小数桁数を補完するには、`POST /{chain}/tokens:batch` を使用します。

* リクエストボディは `{"addresses": [...]}` で、1 回のリクエストにつき最大 100 アドレスを指定できます。100 件を超える場合や、有効な 20 バイトのアドレスでない項目がある場合は、`400 bad_request` を返します（順番に検証し、最初に見つかった無効なアドレスで失敗します）。
* 見つからないアドレスはエラーにならず、`data.missing` に列挙されます。`data.tokens` には、メタデータが見つかったトークンのみが含まれます。
* 重複するアドレスは `tokens` と `missing` の両方で重複排除され、それぞれリクエスト内で最初に出現した順序で並びます。

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


## decimals に応じて数量を換算する

残高フィールド `balance` と ERC-20 送金フィールド `amount` は、10 進数文字列（`UInt256String`）で表された生の整数です。トークンの `total_supply` も、`decimals` に応じた換算が適用されていない、生のオンチェーン整数です。人が読みやすい数量を表示するには、そのトークンの `decimals` に応じて割り算を行います。

* `decimals` は、残高項目自体の `symbol`/`decimals`、または `GET /{chain}/tokens/{token}` と `POST /{chain}/tokens:batch` から取得します。`null` の場合もあります。
* これらの値は `2^53` を超える可能性があるため、JSON 数値で計算しないでください。TypeScript では `BigInt`、Python では `Decimal` を使い、精度の損失を防ぐために 10 進数文字列をそのまま解析します。

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


## データの鮮度

チェーン単位の成功レスポンスには、すべて `meta` が含まれます。

* `as_of_block`：チェーンの、書き込みが完全に完了した最新ブロックです。ブロック単位のエンドポイントは、この高さまでのデータを返します。
* `safe_block`：ノードのコンセンサス上の `safe` ブロックタグを示す指標です（不明な間は `null`）。`finalized_block` を下回ることはなく、レスポンスを切り詰めたり、拒否したり、遅延させたりすることもありません。
* `finalized_block`：ノードのコンセンサス上の `finalized` ブロックタグを示す指標です（不明な間は `null`）。レスポンスを切り詰めたり、拒否したり、遅延させたりすることはありません。この指標に基づいてどの程度の安全性が必要か（確認状況など）をクライアント側で判断します。
* `coverage`：`"full"` または `"partial"`。アドレス送金などのエンドポイントは、`clamp` により返す範囲が狭められた場合や、範囲の開始がチェーンの最初のインデックス済みブロックより前の場合に `"partial"` を返します。
* `refreshed_at`：レスポンスの元となるデータが最後に更新された時刻（UTC）です。`null` の場合もあります。`null` はデータの更新時刻が不明であることを意味し、古いデータとして扱う必要があります。ブロック単位のエンドポイントは常に値を返します。
* `chain`、`chain_slug`、`chain_external_id` も繰り返し含まれます。

よく使われる方法は、最初のレスポンスから `meta.as_of_block` を読み取って最新のインデックス済みブロックまで取得し、確認済みの状態を表示したい場合は `meta.safe_block` / `meta.finalized_block` を確認することです。

## ページ読み込み 1 回あたりの CU 見積もり

各メソッドは、プラットフォームのプラン API から取得した CU 重み付けに基づいて課金されます。

**呼び出しあたりの CU 重み付け**

| メソッド | 呼び出しあたりの CU |
| --- | --- |
| `data.address_balances` | 25 |
| `data.address_transfers` | 25 |
| `data.tokens_batch` | 10 |

**ページを一回読み込む場合（概算）**

残高リクエスト 1 回 + 転送履歴 3 ページ + `tokens:batch` リクエスト 1 回、合計 5 回の呼び出しで約 110 CU です。実際の利用量はページ数とトークン数によって変わります。

課金の判断基準と課金対象外のエラーレスポンスについては、[課金ルール](https://docs.blockvectra.com/en/guides/billing-rules/)を参照してください。必要なものがインデックス済みの送金履歴ではなく最新ブロックのログである場合は、`eth_getLogs` に切り替えるか判断する前に、[最新のノードデータとインデックス済み履歴の比較](https://docs.blockvectra.com/en/guides/logs-vs-transfers/)を読んでください。

## 次のステップ

* [データセット一覧を見る](https://blockvectra.com/en/data/)と、BlockVectra がインデックス化しているすべてのデータセットを確認できます。
* [無料プランと料金を見る](https://blockvectra.com/en/pricing/#free)と、アカウントに含まれる内容を確認できます。
* [コンソールにログイン](https://console.blockvectra.com/login/?next=%2Fkeys%2F)して、API keyを作成します。
