# プログラムによる登録：エージェントと CI のためのウォレット署名ログインと API key 作成

> Source: https://docs.blockvectra.com/ja/guides/programmatic-signup/

ブラウザーのない環境で実行される自律型 AI エージェント、CI パイプライン、自動化スクリプト向けに、BlockVectra はイーサリアムウォレットの署名（EIP-4361 / EIP-191）に基づくプログラムからのログインおよびアカウント作成ワークフローを提供しています。

> **Key security**
>
> 秘密鍵、セッショントークン、API key を AI との会話に貼り付けたり、MCP ツールの引数として渡したりしないでください。


登録する前に、まずキー不要の公開エンドポイント `https://api.blockvectra.com/v1/robinhood_mainnet/public` を試すことができます（ウォレット向け JSON-RPC メソッドのみ利用可能で、Data API にはキーが必要です。メソッドと制限は `/v1/chains` に従います）。クォータが不足している場合はアカウントを登録してください。

## ワークフローの概要

プログラムによる登録とキーのプロビジョニングフローは、4 つのステップで構成されます：

1. **challenge のリクエスト**：`POST /auth/siwe/challenge` にリクエストを送信し、サーバーが生成したサインインメッセージを取得します。
2. **メッセージへの署名**：イーサリアム EOA ウォレットを使用して、EIP-191（`personal_sign`）でメッセージ本文にそのまま署名します。
3. **ログイン / アカウント作成**：メッセージ本文と署名を `POST /auth/siwe/login` に送信します。ウォレットによる初回のサインイン時に、アカウントが自動的に作成されます（`account_created: true`）。新規アカウントは登録時に 30,000,000 CU を受け取れます。クレジットカードは不要です。
4. **API key の作成**：セッショントークンを使用して `POST /keys` を呼び出し、API key を作成します。

## そのまま実行可能な完全なコード例

ここから開始：ローカルのイーサリアム EOA 署名ツールを使用し、キーを作成して、eth\_blockNumber で検証します。
Bash の例では、curl、jq、Foundry cast が必要です。ウォレットの認証情報はローカルの署名環境に安全に保管してください。

スターターテンプレートの完全版：[blockvectra/agent-quickstart](https://github.com/blockvectra/agent-quickstart)

以下のスクリプトは、ウォレットの認証情報を読み取り、challenge とログインのシーケンスを完了し、API key をプロビジョニングし、環境設定用の `export BLOCKVECTRA_API_KEY=...` をエクスポートまたは出力して、検証用の `eth_blockNumber` リクエストを送信します：

新しいキーが有効になるまで数秒かかります。これらの例では自動的に再試行が行われます。

**Bash**

```bash
BASE=https://console-api.blockvectra.com/v1
# $ADDR: Ethereum wallet address (0x...)
# $PK: wallet private key, loaded from a secrets manager (never hardcode in scripts)

# 1. Fetch server-generated SIWE message (omit Origin header)
curl -s "$BASE/auth/siwe/challenge" -H 'Content-Type: application/json' \
  -d "{\"address\":\"$ADDR\",\"purpose\":\"login\"}" > challenge.json
jq -r .message challenge.json > msg.txt

# 2. Sign the exact message with EIP-191 personal_sign
SIG=$(cast wallet sign --private-key "$PK" "$(cat msg.txt)")

# 3. Submit verbatim message and signature (omit Origin header) to log in (ref is optional)
jq -n --rawfile m msg.txt --arg s "$SIG" '{message: ($m | rtrimstr("\n")), signature: $s, ref: "docs-signup"}' |
  curl -s "$BASE/auth/siwe/login" -H 'Content-Type: application/json' -d @- > session.json
TOKEN=$(jq -r .session.token session.json)

# 4. Create an API key (the secret is returned only once)
KEY_RESP=$(curl -s "$BASE/keys" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"label":"agent-key"}')
export BLOCKVECTRA_API_KEY=$(echo "$KEY_RESP" | jq -r .api_key)

# 5. Call JSON-RPC with the key in the x-api-key request header
RPC_DEADLINE=$((SECONDS + 10))
while true; do
  RPC_TIMEOUT=$((RPC_DEADLINE - SECONDS))
  if ((RPC_TIMEOUT <= 0)); then
    printf '%s' "${RPC_BODY:-}"
    break
  fi
  RPC_RESP=$(curl -s --max-time "$RPC_TIMEOUT" -w '\n%{http_code}' "https://api.blockvectra.com/v1/robinhood_mainnet" \
    -H "x-api-key: $BLOCKVECTRA_API_KEY" \
    -H 'Content-Type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}') || { rc=$?; echo "request failed (curl exit $rc)" >&2; exit $rc; }
  RPC_STATUS=${RPC_RESP##*$'\n'}
  RPC_BODY=${RPC_RESP%$'\n'*}
  if ((SECONDS + 2 < RPC_DEADLINE)) &&
    printf '%s' "$RPC_BODY" | jq -e --arg status "$RPC_STATUS" '
      ($status == "401" and .error.data.reason == "invalid_api_key") or
      ($status == "503" and .error.code == -32021)
    ' >/dev/null 2>&1; then
    sleep 2
  else
    printf '%s' "$RPC_BODY"
    break
  fi
done
```


  **TypeScript**

```bash
npm i viem
```

```ts
// Requires ESM (top-level await; run with node --input-type=module or tsx)
import { privateKeyToAccount } from "viem/accounts";

const BASE = "https://console-api.blockvectra.com/v1";
const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
const account = privateKeyToAccount(privateKey);

// 1. Fetch server-generated SIWE message (omit Origin header)
const challengeRes = await fetch(`${BASE}/auth/siwe/challenge`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ address: account.address, purpose: "login" }),
});
if (!challengeRes.ok) throw new Error(`Challenge failed: ${challengeRes.status}`);
const { message } = (await challengeRes.json()) as { message: string };

// 2. Sign the exact message with EIP-191 personal_sign
const signature = await account.signMessage({ message });

// 3. Submit verbatim message and signature (omit Origin header) to log in (ref is optional)
const loginRes = await fetch(`${BASE}/auth/siwe/login`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message, signature, ref: "docs-signup" }),
});
if (!loginRes.ok) throw new Error(`Login failed: ${loginRes.status}`);
const { session } = (await loginRes.json()) as { session: { token: string } };

// 4. Create an API key (the secret is returned only once)
const keyRes = await fetch(`${BASE}/keys`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${session.token}`,
  },
  body: JSON.stringify({ label: "agent-key" }),
});
if (!keyRes.ok) throw new Error(`Create key failed: ${keyRes.status}`);
const { api_key } = (await keyRes.json()) as { api_key: string };
console.log("Created API key:", api_key);
console.log(`export BLOCKVECTRA_API_KEY=${api_key}`);

// 5. Call JSON-RPC with the key in the x-api-key request header
const rpcDeadline = performance.now() + 10_000;
while (true) {
  const rpcRes = await fetch("https://api.blockvectra.com/v1/robinhood_mainnet", {
    method: "POST",
    signal: AbortSignal.timeout(Math.max(1, Math.ceil(rpcDeadline - performance.now()))),
    headers: {
      "Content-Type": "application/json",
      "x-api-key": api_key,
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "eth_blockNumber",
      params: [],
    }),
  });
  const rpcBody = await rpcRes.json();
  const retryable =
    (rpcRes.status === 401 && rpcBody.error?.data?.reason === "invalid_api_key") ||
    (rpcRes.status === 503 && rpcBody.error?.code === -32021);
  if (retryable && performance.now() + 2_000 < rpcDeadline) {
    await new Promise((resolve) => setTimeout(resolve, 2_000));
    continue;
  }
  if (!rpcRes.ok) throw new Error(`RPC call failed: ${rpcRes.status}`);
  console.log("Block number response:", rpcBody);
  break;
}
```


  **Python**

```bash
pip install eth-account requests
```

```python
import os
import time
import requests
from eth_account import Account
from eth_account.messages import encode_defunct

BASE = "https://console-api.blockvectra.com/v1"
private_key = os.environ["PRIVATE_KEY"]
account = Account.from_key(private_key)
address = account.address

# 1. Fetch server-generated SIWE message (omit Origin header)
challenge_resp = requests.post(
    f"{BASE}/auth/siwe/challenge",
    json={"address": address, "purpose": "login"},
)
challenge_resp.raise_for_status()
message = challenge_resp.json()["message"]

# 2. Sign the exact message with EIP-191 personal_sign
signable = encode_defunct(text=message)
signed = Account.sign_message(signable, private_key=private_key)
signature = "0x" + bytes(signed.signature).hex()

# 3. Submit verbatim message and signature (omit Origin header) to log in (ref is optional)
login_resp = requests.post(
    f"{BASE}/auth/siwe/login",
    json={"message": message, "signature": signature, "ref": "docs-signup"},
)
login_resp.raise_for_status()
token = login_resp.json()["session"]["token"]

# 4. Create an API key (the secret is returned only once)
key_resp = requests.post(
    f"{BASE}/keys",
    headers={"Authorization": f"Bearer {token}"},
    json={"label": "agent-key"},
)
key_resp.raise_for_status()
api_key = key_resp.json()["api_key"]
print("Created API key:", api_key)
print(f"export BLOCKVECTRA_API_KEY={api_key}")

# 5. Call JSON-RPC with the key in the x-api-key request header
rpc_deadline = time.monotonic() + 10
while True:
    rpc_resp = requests.post(
        "https://api.blockvectra.com/v1/robinhood_mainnet",
        headers={"x-api-key": api_key, "Content-Type": "application/json"},
        json={"jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": []},
        timeout=max(0.001, rpc_deadline - time.monotonic()),
    )
    rpc_data = rpc_resp.json()
    error = rpc_data.get("error") or {}
    retryable = (
        rpc_resp.status_code == 401
        and (error.get("data") or {}).get("reason") == "invalid_api_key"
    ) or (rpc_resp.status_code == 503 and error.get("code") == -32021)
    if retryable and time.monotonic() + 2 < rpc_deadline:
        time.sleep(2)
        continue
    rpc_resp.raise_for_status()
    print("Block number response:", rpc_data)
    break
```


## Base URL とプログラムモード

すべての認証およびキー管理エンドポイントは、公式のベース URL を使用します：

```
https://console-api.blockvectra.com/v1
```

### Origin ヘッダーの省略

プログラムによるリクエストは**プログラムモード**で動作します：

* challenge（`POST /auth/siwe/challenge`）と login（`POST /auth/siwe/login`）の両方のリクエストで、**`Origin` ヘッダーを含めてはなりません**（`curl` や標準的な HTTP クライアントはデフォルトでこのヘッダーを省略します。手動で追加しないでください）。
* `Origin` ヘッダーが送信され、それが設定済みの Web コンソールドメインでない場合（空文字列や `null` を含む）、challenge リクエストは HTTP 400 `invalid_request` を返します。
* ログイン時のモードが challenge 時のモードと一致しない場合（例えば、`Origin` なしでプログラム用 challenge をリクエストした後に `Origin` ヘッダー付きでログインを送信した場合、またはその逆）、login リクエストは HTTP 400 `siwe_invalid`（`reason: domain_mismatch`）を返します。

### メッセージの完全性とウォレットの要件

* **メッセージの改変禁止とそのままの送信**：クライアントは、challenge エンドポイントから返されたメッセージテキストをそのまま署名し送信する必要があります。空白、ドメイン、チェーン ID、その他のフィールドを一切変更しないでください。変更を加えると、HTTP 400 `siwe_invalid`（`reason: signature`）が返されます。
* **対応ウォレット**：イーサリアムメインネット（チェーン ID 1）の EOA（Externally Owned Account）。署名は 65 バイトの ECDSA 署名（`personal_sign`）である必要があります。コントラクトウォレット（EIP-1271）およびスマートアカウントはサポートされていません。
* **challenge の有効期間**：各 challenge の nonce は 1 回限り有効で、5 分後に失効します。

### リクエストボディと登録時のアトリビューション（任意）

`POST /auth/siwe/login` のリクエストボディには、必須の認証パラメーターと任意の登録アトリビューションフィールドを指定できます：

* **必須フィールド**：
  * `message`：challenge エンドポイントから取得した完全な SIWE メッセージ文字列。
  * `signature`：イーサリアムウォレットで EIP-191 を用いて `message` に署名して生成された、65 バイトの 16 進数署名（`0x` プレフィックス付き）。
* **任意のアトリビューションフィールド**（新規アカウント作成時に 1 回のみ保存され、それ以降のログインでは無視されます）：
  * `ref`：`^[a-z0-9._-]{1,64}$` に一致する小文字のチャネルトークン（小文字の ASCII 英数字、`.`、`_`、`-`、1〜64 文字）。例えば自律型エージェントは、フレームワークやランタイム識別子（例：`my-agent.v1`）を設定できます。大文字、空文字列、文字数超過、非対応文字を含む不適合な値は、大文字小文字の変換を行わずに HTTP 400 `invalid_request` を返し、アカウント作成をブロックします。該当しない場合は省略するか `null` を渡してください。
  * `referrer`：流入元の URL またはホスト名文字列。文字列以外の型の場合にのみ HTTP 400 を返します。

`signup_method` などの未定義フィールドを送信すると、HTTP 400 `invalid_request` が返されます。

## セッショントークンと API key

### セッショントークンのライフサイクル

* **形式**：`rgs_` に続く 64 文字の小文字 16 進数文字列。
* **有効期間**：絶対的な有効期間は 7 日間。24 時間アイドル状態が続くと自動的に失効します。
* **リフレッシュトークンなし**：セッショントークンが失効した場合は、新しい challenge とログインフローを開始してください。
* **ヘッダー**：リクエストヘッダー `Authorization: Bearer rgs_...` でセッショントークンを渡します。

### API key の作成

* セッショントークンを指定して `POST /keys` を呼び出し、API key（`rgw_` に続く 64 文字の 16 進数文字列）を作成します。
* 1 つのアカウントにつき、失効しておらず有効期限内のキー（`active` + `disabled`）は最大 20 個まで保持できます。期限切れのキーはカウントされません。これを超えると、HTTP 409 `key_limit_reached`（`reason: active_keys`、`limit: 20`）が返されます。先に不要なキーを失効させてください。この上限は、そのアカウントのすべての識別子、セッション、チェーンに適用されます。キーの作成とローテーションも 24 時間あたり最大 20 回に制限されており、これを超えると HTTP 429 `rate_limited`（`Retry-After: 3600`）が返されます。
* 任意の上限と有効期限：`cu_cap`（キーのライフタイム CU 上限、ソフトキャップ）および有効期限（`expires_in_secs` または `expires_at`、キーポリシーで許可されている最大日数まで）を指定できます。有効期限が切れるか上限に達すると、サーバーは 403（JSON-RPC `-32025`、理由 `key_expired` または `key_cap_exhausted`）を返します。
* シークレットである `api_key` は**作成時に 1 度だけ返されます**。シークレットマネージャーや環境変数に直ちに安全に保管してください。
* 1 つの API key は、JSON-RPC および Data API において、サポートされているすべてのチェーンで共通して利用できます。

## セッションまたは API key を紛失した場合

BlockVectra では、**エージェントのアカウント識別子は登録時に使用したイーサリアムウォレットアドレスに紐付けられます**。セッショントークンが失効した場合や、API key が紛失・漏洩した場合でも、そのウォレットのみを使用して完全な制御を回復できます：

1. **同じウォレットで再認証する**：challenge をリクエストし、同じウォレットで署名して、ログインリクエスト（`POST /auth/siwe/login`）を送信します。サーバーは署名を検証し、`account_created: false` で既存のアカウントにログインして、新しいセッショントークンを発行します。
2. **新しい API key を作成する**：新しいセッショントークンを使用し、`Authorization: Bearer <token>` ヘッダーを付けて `POST /keys` に `{"label": "..."}` を送信します。エンドポイントは HTTP 201 を返し、作成されたキーの詳細が `key` に、一度限りのシークレットが `api_key` に含まれます。このキーを直ちに環境変数またはシークレットマネージャーに保存してください。
3. **アカウントのすべてのキーを一覧表示する**：
   * エンドポイント：`GET /keys`
   * ヘッダー：`Authorization: Bearer <token>`
   * クエリパラメーター：任意の `include_revoked=true`（`true` の場合、失効したキーを含めます。デフォルトは active / disabled のキーのみ）。
   * レスポンス：HTTP 200 と JSON `{"items": [...]}`。`items` 配列の各要素には以下が含まれます：
     * `key_id`：一意のキー識別子（文字列）
     * `label`：キーのラベル（文字列または `null`）
     * `status`：ステータス（`"active"`、`"disabled"`、または `"revoked"`）
     * `created_at`：作成日時（ISO 8601 文字列）
     * `revoked_at`：失効日時（文字列、未失効の場合は `null`）
4. **未使用または漏洩したキーを失効させる**：
   * エンドポイント：`POST /keys/{key_id}/revoke`（注意：パスに対象の `key_id` を指定した `POST` を使用し、リクエストボディは空にします）
   * ヘッダー：`Authorization: Bearer <token>`
   * 動作：冪等です。`active` または `disabled` ステータスのキーをいずれも失効させることができます。すでに失効している場合は、変更なしで HTTP 200 を返します。失効後、そのキーを使用したリクエストは拒否されます。
   * レスポンス：失効したキーオブジェクトを返す HTTP 200（フィールドは上記のキーオブジェクトと一致し、`status: "revoked"` および `revoked_at` にタイムスタンプが含まれます）。

> **Key and secret security**
>
> ウォレットの秘密鍵と API key は環境変数またはシークレットマネージャーに保管してください。コードリポジトリにコミットしたり、ログに出力したり、AI チャットの会話に貼り付けたりしないでください。


## セキュリティ上の推奨事項

* **短期キーを使用し完了時に失効させる**：自動化タスクや一時的なタスクでは、`expires_in_secs` で有効期間の短いキーを作成し、作業完了後に `POST /keys/{key_id}/revoke` で直ちに失効させてください。

## 登録レート制限（`signup_rate_limited`）

アカウント作成には登録レート制限が適用されます。IP ごとのトークンバケットは 100 アカウントの容量を持ち、IPv4 アドレスまたは IPv6 /64 プレフィックスごとに 1 時間あたり 100 アカウントの割合で補充され、SIWE と OAuth の登録で共有されます：

* 登録制限を超過した場合、`POST /auth/siwe/login` は HTTP 429 `signup_rate_limited` を返し、`Retry-After` ヘッダーで待機秒数を示します。
* `reason` フィールドによって制限の適用範囲が区別されます：
  * `per_ip`：リクエスト元の IP プレフィックスに対する登録枠が使い果たされた場合。
  * `global`：プラットフォーム全体の登録上限が使い果たされた場合。
* 登録レート制限は新規アカウントの登録時のみ評価されます。既存アカウントのログインが登録レート制限によってブロックされることはありません。

## 関連リソース

* キー不要の MCP サーバーや機械可読なコンテキストファイルについては、[AI エージェント連携ガイド](https://docs.blockvectra.com/en/guides/ai-agents/)を参照してください。
* 複数言語によるクライアントの例については、[クイックスタート](https://docs.blockvectra.com/en/quickstart/)を確認してください。
* すべてのエラーコード、理由、および自動復旧アクションについては、[エラーリファレンス](https://docs.blockvectra.com/en/errors/)を参照してください。

## 次のステップ

* `x-api-key: $BLOCKVECTRA_API_KEY` を指定して、最初の JSON-RPC または Data API コールを送信します。
* `GET /v1/account` を使用して、[アカウント残高と制限を確認](https://docs.blockvectra.com/en/guides/ai-agents/#query-balance-get-v1account)します。
* 残高を維持するには、[Agent プログラムによるチャージガイド](https://docs.blockvectra.com/en/guides/agent-topup/)に従ってください。
