# 程式化註冊：為 Agent 與 CI 設計的錢包登入與 API key 建立

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

針對在無瀏覽器環境中運作的自主 AI Agent、CI 流水線與自動化指令碼，BlockVectra 提供以以太坊錢包簽章（EIP-4361 / EIP-191）為基礎的程式化登入與建立帳戶流程。

> **金鑰安全**
>
> 切勿將私鑰、session 權杖或 API key 貼進與 AI 的對話，或作為 MCP 工具引數傳入。


註冊前，你可以先試用免 key 的公開端點 `https://api.blockvectra.com/v1/robinhood_mainnet/public`（僅限錢包類 JSON-RPC 方法，Data API 需要 key；方法與限制以 `/v1/chains` 為準）；額度不足時再註冊帳戶。

## 流程總覽

程式化註冊與 key 佈建流程包含四個步驟：

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**：使用 session 權杖呼叫 `POST /keys` 建立 API key。

## 完整可執行範例

從這裡開始：使用本機的以太坊 EOA 簽章工具、建立 key，並以 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` 請求：

新 key 需要幾秒鐘才會生效；這些範例會自動重試。

**Bash**

```bash
BASE=https://console-api.blockvectra.com/v1
# $ADDR: 以太坊錢包地址 (0x...)
# $PK: 錢包私鑰，從機密管理服務載入（切勿寫死在指令碼中）

# 1. 取得伺服器產生的 SIWE 訊息（省略 Origin 標頭）
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. 以 EIP-191 personal_sign 對訊息原文簽章
SIG=$(cast wallet sign --private-key "$PK" "$(cat msg.txt)")

# 3. 原樣提交訊息與簽章（省略 Origin 標頭）以登入（ref 為選填）
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. 建立 API key（secret 僅回傳一次）
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. 以 x-api-key 請求標頭中的 key 呼叫 JSON-RPC
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
// 需要 ESM（頂層 await；使用 node --input-type=module 或 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. 取得伺服器產生的 SIWE 訊息（省略 Origin 標頭）
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. 以 EIP-191 personal_sign 對訊息原文簽章
const signature = await account.signMessage({ message });

// 3. 原樣提交訊息與簽章（省略 Origin 標頭）以登入（ref 為選填）
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. 建立 API key（secret 僅回傳一次）
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. 以 x-api-key 請求標頭中的 key 呼叫 JSON-RPC
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. 取得伺服器產生的 SIWE 訊息（省略 Origin 標頭）
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. 以 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. 原樣提交訊息與簽章（省略 Origin 標頭）以登入（ref 為選填）
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. 建立 API key（secret 僅回傳一次）
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. 以 x-api-key 請求標頭中的 key 呼叫 JSON-RPC
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 與程式化模式

所有認證與 key 管理端點都使用官方 Base URL：

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

### 省略 Origin 標頭

程式化請求以**程式化模式**運作：

* challenge（`POST /auth/siwe/challenge`）與 login（`POST /auth/siwe/login`）請求都**不得包含 `Origin` 標頭**（`curl` 與標準 HTTP 用戶端預設會省略此標頭；請勿手動加入）。
* 若送出的 `Origin` 標頭不是已設定的網頁控制台網域（包含空字串或 `null`），challenge 請求會回傳 HTTP 400 `invalid_request`。
* 若登入時的模式與 challenge 時的模式不符（例如以不含 `Origin` 的方式取得程式化 challenge 後，提交 login 時附帶 `Origin` 標頭，反之亦然），login 請求會回傳 HTTP 400 `siwe_invalid`，並帶有 `reason: domain_mismatch`。

### 訊息完整性與錢包要求

* **原樣簽章與提交**：用戶端必須對 challenge 端點回傳的訊息原文簽章並原樣提交。請勿更動空白、網域、鏈 ID 或任何欄位。任何修改都會導致 HTTP 400 `siwe_invalid`，並帶有 `reason: signature`。
* **支援的錢包**：以太坊主網（Chain ID 1）的外部持有帳戶（EOA）。簽章必須是 65 位元組的 ECDSA 簽章（`personal_sign`）。不支援合約錢包（EIP-1271）與智慧帳戶。
* **challenge 有效期**：每個 challenge nonce 僅限使用一次，並在 5 分鐘後過期。

### 請求主體與註冊歸因（選填）

`POST /auth/siwe/login` 的請求主體接受必填的認證參數與選填的註冊歸因欄位：

* **必填欄位**：
  * `message`：從 challenge 端點取得的完整 SIWE 訊息字串。
  * `signature`：使用以太坊錢包透過 EIP-191 對 `message` 簽章所產生的 65 位元組十六進位簽章（`0x` 開頭）。
* **選填歸因欄位**（僅在建立新帳戶時保存一次；後續登入時忽略）：
  * `ref`：符合 `^[a-z0-9._-]{1,64}$` 的小寫渠道權杖（小寫 ASCII 字母、數字、`.`、`_`、`-`，長度 1–64 個字元）。例如自主 Agent 可將其設為自身的框架或執行環境識別碼（如 `my-agent.v1`）。不合規的值（包含大寫字母、空字串、長度過長或不支援的字元）會在不進行大小寫折疊的情況下回傳 HTTP 400 `invalid_request`，並阻止建立帳戶；不適用時請省略或傳入 `null`。
  * `referrer`：來源 URL 或主機名字串；僅非字串型別會回傳 HTTP 400。

送出 `signup_method` 等未定義欄位會回傳 HTTP 400 `invalid_request`。

## Session 權杖與 API key

### Session 權杖生命週期

* **格式**：`rgs_` 後接 64 個小寫十六進位字元。
* **有效期**：絕對存續時間為 7 天；閒置 24 小時後自動過期。
* **無 refresh token**：session 權杖過期時，重新發起 challenge 與 login 流程。
* **標頭**：在 `Authorization: Bearer rgs_...` 請求標頭中傳入 session 權杖。

### 建立 API key

* 使用 session 權杖呼叫 `POST /keys` 建立 API key（`rgw_` 後接 64 個十六進位字元）。
* 每個帳戶最多可有 20 把未撤銷、未過期（`active` + `disabled`）的 key；已過期的 key 不計入。超過此數會回傳 HTTP 409 `key_limit_reached`，並帶有 `reason: active_keys` 與 `limit: 20`；請先撤銷一把 key。此上限適用於帳戶的所有身分、session 與鏈。建立與輪替 key 也限制為每 24 小時 20 次；超過時會回傳 HTTP 429 `rate_limited`，並附帶 `Retry-After: 3600`。
* 選填的上限與到期：你可以提供 `cu_cap`（該 key 的終身 CU 上限，屬於軟性上限）與到期時間（`expires_in_secs` 或 `expires_at`，最長為 key 政策允許的天數）；一旦過期或用盡上限，伺服器會回傳 403（JSON-RPC `-32025`，原因為 `key_expired` 或 `key_cap_exhausted`）。
* secret `api_key` **僅在建立時回傳一次**。請立即妥善保存至機密管理服務或環境變數中。
* 一把 API key 適用於 JSON-RPC 與 Data API 上所有支援的鏈。

## Session 或 API key 遺失了？

在 BlockVectra 中，**Agent 的帳戶身分與註冊時使用的以太坊錢包地址綁定**。若你的 session 權杖過期，或 API key 遺失、洩漏，只要持有該錢包即可恢復完整控制權：

1. **使用同一個錢包重新認證**：要求 challenge、使用同一錢包簽章，並提交 login 請求（`POST /auth/siwe/login`）。伺服器會驗證簽章，以 `account_created: false` 登入既有帳戶，並核發新的 session 權杖。
2. **建立新的 API key**：使用新的 session 權杖，以 `{"label": "..."}` 與 `Authorization: Bearer <token>` 標頭呼叫 `POST /keys`。端點會回傳 HTTP 201，並在 `key` 中帶有已建立 key 的詳細資料，在 `api_key` 中帶有一次性的 secret。請立即將此 key 保存至環境變數或機密管理服務中。
3. **列出帳戶的所有 key**：
   * 端點：`GET /keys`
   * 標頭：`Authorization: Bearer <token>`
   * 查詢參數：選填 `include_revoked=true`（為 `true` 時包含已撤銷的 key；預設僅包含 active/disabled 的 key）。
   * 回應：HTTP 200 與 JSON `{"items": [...]}`。`items` 陣列中的每個元素包含：
     * `key_id`：key 唯一識別碼（字串）
     * `label`：key 標籤（字串或 `null`）
     * `status`：狀態（`"active"`、`"disabled"` 或 `"revoked"`）
     * `created_at`：建立時間戳（ISO 8601 字串）
     * `revoked_at`：撤銷時間戳（字串；未撤銷時為 `null`）
4. **撤銷未使用或已外洩的 key**：
   * 端點：`POST /keys/{key_id}/revoke`（注意：使用 `POST`，目標 `key_id` 放在路徑中；請求主體為空）
   * 標頭：`Authorization: Bearer <token>`
   * 行為：冪等；`active` 或 `disabled` 狀態的 key 都可撤銷。若已撤銷，則原樣回傳 HTTP 200。撤銷後，使用該 key 的請求會被拒絕。
   * 回應：HTTP 200，回傳已撤銷的 key 物件（欄位與上述 key 物件相同，`status` 為 `"revoked"`，`revoked_at` 帶有時間戳）。

> **Key 與 secret 安全**
>
> 請將錢包私鑰與 API key 保存在環境變數或機密管理服務中。切勿提交到程式碼倉庫、寫入日誌，或貼進與 AI 的對話。


## 安全性建議

* **使用短期 key 並在用完後撤銷**：針對自動化或短暫任務，使用 `expires_in_secs` 建立短期 key，並在完成工作後立即透過 `POST /keys/{key_id}/revoke` 撤銷。

## 註冊速率限制（`signup_rate_limited`）

建立帳戶受註冊速率限制約束。每個 IP 的權杖桶容量為 100 個帳戶，並以每個 IPv4 地址或 IPv6 /64 前綴每小時 100 個帳戶的速率補充，由 SIWE 與 OAuth 註冊共用：

* 超過註冊限制時，`POST /auth/siwe/login` 會回傳 HTTP 429 `signup_rate_limited`，並附帶 `Retry-After` 標頭，指出需等待的秒數。
* `reason` 欄位區分限制範圍：
  * `per_ip`：發出請求的 IP 前綴的註冊額度已用盡。
  * `global`：平台的整體註冊上限已用盡。
* 註冊速率限制僅評估新帳戶註冊。既有帳戶登入不會被註冊速率限制阻擋。

## 相關資源

* 閱讀 [AI Agent 整合指南](https://docs.blockvectra.com/zh-hant/guides/ai-agents/)，了解免 key 的 MCP 伺服器與機器可讀的脈絡檔案。
* 參閱[快速入門](https://docs.blockvectra.com/zh-hant/quickstart/)，取得多語言用戶端範例。
* 查看[錯誤參考](https://docs.blockvectra.com/zh-hant/errors/)，取得完整的錯誤碼、原因與自動化復原動作。

## 下一步

* 以 `x-api-key: $BLOCKVECTRA_API_KEY` 送出你的第一個 JSON-RPC 或 Data API 呼叫。
* 使用 `GET /v1/account` [查詢帳戶餘額與限制](https://docs.blockvectra.com/zh-hant/guides/ai-agents/#query-balance-get-v1account)。
* 依照 [Agent 程式化儲值指南](https://docs.blockvectra.com/zh-hant/guides/agent-topup/)維持餘額。
