# Đăng ký bằng lập trình: đăng nhập bằng ví và tạo API key cho Agent và CI

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

Đối với AI Agent tự chủ, pipeline CI và script tự động chạy không cần trình duyệt, BlockVectra cung cấp quy trình đăng nhập và mở tài khoản bằng lập trình dựa trên chữ ký ví Ethereum (EIP-4361 / EIP-191).

> **Key security**
>
> Không bao giờ dán khóa riêng, session token hoặc API key vào cuộc trò chuyện với AI hay truyền chúng dưới dạng đối số công cụ MCP.


Trước khi đăng ký, bạn có thể thử endpoint công khai không cần API key `https://api.blockvectra.com/v1/robinhood_mainnet/public` (chỉ các phương thức JSON-RPC dành cho ví, Data API cần API key; phương thức và giới hạn theo `/v1/chains`); đăng ký tài khoản nếu hạn mức không đủ.

## Tổng quan quy trình

Quy trình đăng ký và cấp API key bằng lập trình gồm bốn bước:

1. **Yêu cầu challenge**: Gửi yêu cầu tới `POST /auth/siwe/challenge` để lấy thông điệp đăng nhập do máy chủ tạo.
2. **Ký thông điệp**: Ký chính xác thông điệp bằng ví Ethereum EOA qua EIP-191 (`personal_sign`).
3. **Đăng nhập / mở tài khoản**: Gửi nguyên văn thông điệp và chữ ký tới `POST /auth/siwe/login`. Khi ví đăng nhập lần đầu, tài khoản được tạo tự động (`account_created: true`). Tài khoản mới nhận 30,000,000 CU khi đăng ký — không cần thẻ tín dụng.
4. **Tạo API key**: Dùng session token để gọi `POST /keys` và tạo API key.

## Ví dụ đầy đủ có thể chạy

Bắt đầu tại đây: dùng bộ ký Ethereum EOA cục bộ, tạo API key và xác minh bằng eth\_blockNumber.
Ví dụ Bash cần curl, jq và Foundry cast. Giữ thông tin xác thực ví trong môi trường ký cục bộ của bạn.

Mẫu khởi đầu đầy đủ: [blockvectra/agent-quickstart](https://github.com/blockvectra/agent-quickstart)

Các script sau đọc thông tin xác thực ví, hoàn tất chuỗi challenge và đăng nhập, cấp API key, xuất hoặc in `export BLOCKVECTRA_API_KEY=...` để cấu hình môi trường, rồi gửi yêu cầu `eth_blockNumber` để xác minh:

API key mới cần vài giây để có hiệu lực; các ví dụ này tự động thử lại.

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


## URL cơ sở và chế độ lập trình

Tất cả endpoint xác thực và quản lý API key dùng URL cơ sở chính thức:

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

### Bỏ header Origin

Yêu cầu bằng lập trình hoạt động trong **chế độ lập trình**:

* Cả yêu cầu challenge (`POST /auth/siwe/challenge`) và đăng nhập (`POST /auth/siwe/login`) đều **không được chứa header `Origin`** (`curl` và HTTP client tiêu chuẩn mặc định bỏ header này; không thêm thủ công).
* Nếu gửi header `Origin` nhưng giá trị không phải miền bảng điều khiển web đã cấu hình (kể cả chuỗi rỗng hoặc `null`), yêu cầu challenge trả HTTP 400 `invalid_request`.
* Nếu chế độ khi đăng nhập không khớp chế độ challenge (ví dụ yêu cầu challenge bằng lập trình không có `Origin`, sau đó gửi đăng nhập có header `Origin`, hoặc ngược lại), yêu cầu đăng nhập trả HTTP 400 `siwe_invalid` với `reason: domain_mismatch`.

### Tính toàn vẹn thông điệp và yêu cầu đối với ví

* **Ký và gửi nguyên văn**: Client phải ký và gửi văn bản thông điệp chính xác như endpoint challenge trả về. Không thay đổi khoảng trắng, miền, Chain ID hay bất kỳ trường nào. Mọi thay đổi đều dẫn đến HTTP 400 `siwe_invalid` với `reason: signature`.
* **Ví được hỗ trợ**: Tài khoản do cá nhân sở hữu (EOA) trên Ethereum mainnet (Chain ID 1). Chữ ký phải là chữ ký ECDSA 65 byte (`personal_sign`). Không hỗ trợ ví hợp đồng (EIP-1271) và tài khoản thông minh.
* **Hiệu lực challenge**: Mỗi nonce challenge chỉ dùng một lần và hết hạn sau 5 phút.

### Body yêu cầu và nguồn đăng ký (tùy chọn)

Body yêu cầu `POST /auth/siwe/login` chấp nhận tham số xác thực bắt buộc và các trường nguồn đăng ký tùy chọn:

* **Trường bắt buộc**:
  * `message`: Chuỗi thông điệp SIWE đầy đủ lấy từ endpoint challenge.
  * `signature`: Chữ ký thập lục phân 65 byte (có tiền tố `0x`) tạo bằng cách ký `message` qua EIP-191 bằng ví Ethereum.
* **Trường nguồn đăng ký tùy chọn** (chỉ lưu một lần khi tạo tài khoản mới; bỏ qua trong các lần đăng nhập sau):
  * `ref`: Token kênh viết thường khớp `^[a-z0-9._-]{1,64}$` (chữ cái ASCII viết thường, chữ số, `.`, `_`, `-`, 1–64 ký tự). Ví dụ, Agent tự chủ có thể đặt giá trị này thành mã định danh framework hoặc runtime (như `my-agent.v1`). Giá trị không hợp lệ (gồm chữ hoa, chuỗi rỗng, quá dài hoặc ký tự không được hỗ trợ) trả HTTP 400 `invalid_request` mà không chuyển đổi chữ hoa thành chữ thường và ngăn tạo tài khoản; bỏ qua hoặc truyền `null` nếu không áp dụng.
  * `referrer`: Chuỗi URL nguồn hoặc hostname; chỉ kiểu không phải chuỗi mới trả HTTP 400.

Gửi trường chưa được định nghĩa như `signup_method` trả HTTP 400 `invalid_request`.

## Session token và API key

### Vòng đời session token

* **Định dạng**: `rgs_` theo sau bởi 64 ký tự thập lục phân viết thường.
* **Hiệu lực**: Thời gian tồn tại tuyệt đối là 7 ngày; tự động hết hạn sau 24 giờ không hoạt động.
* **Không có refresh token**: Khi session token hết hạn, bắt đầu quy trình challenge và đăng nhập mới.
* **Header**: Truyền session token trong header yêu cầu `Authorization: Bearer rgs_...`.

### Tạo API key

* Gọi `POST /keys` bằng session token để tạo API key (`rgw_` theo sau bởi 64 ký tự thập lục phân).
* Mỗi tài khoản có tối đa 20 API key chưa bị thu hồi và chưa hết hạn (`active` + `disabled`); API key hết hạn không được tính. Vượt giới hạn trả HTTP 409 `key_limit_reached` với `reason: active_keys` và `limit: 20`; hãy thu hồi một API key trước. Giới hạn áp dụng trên tất cả danh tính, phiên và chuỗi của tài khoản. Tạo và xoay vòng API key cũng bị giới hạn ở 20 lần mỗi 24 giờ; vượt giới hạn trả HTTP 429 `rate_limited` với `Retry-After: 3600`.
* Hạn mức và thời hạn tùy chọn: bạn có thể cung cấp `cu_cap` (hạn mức CU trọn đời của API key, là hạn mức mềm) và thời hạn (`expires_in_secs` hoặc `expires_at`, tối đa số ngày chính sách API key cho phép); khi hết hạn hoặc dùng hết hạn mức, máy chủ trả 403 (JSON-RPC `-32025`, reason là `key_expired` hoặc `key_cap_exhausted`).
* Giá trị bí mật `api_key` **chỉ được trả một lần khi tạo**. Lưu an toàn ngay vào trình quản lý bí mật hoặc biến môi trường.
* Một API key hoạt động trên tất cả chuỗi được hỗ trợ cho JSON-RPC và Data API.

## Mất session token hoặc API key?

Trong BlockVectra, **danh tính tài khoản của Agent gắn với địa chỉ ví Ethereum dùng khi đăng ký**. Nếu session token hết hạn hoặc API key bị mất hay lộ, bạn có thể khôi phục toàn quyền kiểm soát chỉ bằng ví đó:

1. **Xác thực lại bằng cùng ví**: Yêu cầu challenge, ký bằng cùng ví và gửi yêu cầu đăng nhập (`POST /auth/siwe/login`). Máy chủ xác minh chữ ký, đăng nhập tài khoản hiện có với `account_created: false` và cấp session token mới.
2. **Tạo API key mới**: Dùng session token mới để gọi `POST /keys` với `{"label": "..."}` và header `Authorization: Bearer <token>`. Endpoint trả HTTP 201 với chi tiết API key đã tạo trong `key` và giá trị bí mật chỉ trả một lần trong `api_key`. Lưu API key này ngay vào biến môi trường hoặc trình quản lý bí mật.
3. **Liệt kê tất cả API key của tài khoản**:
   * Endpoint: `GET /keys`
   * Header: `Authorization: Bearer <token>`
   * Tham số truy vấn: `include_revoked=true` tùy chọn (khi là `true`, bao gồm API key đã thu hồi; mặc định chỉ gồm API key active/disabled).
   * Phản hồi: HTTP 200 với JSON `{"items": [...]}`. Mỗi phần tử trong mảng `items` gồm:
     * `key_id`: mã định danh API key duy nhất (chuỗi)
     * `label`: nhãn API key (chuỗi hoặc `null`)
     * `status`: trạng thái (`"active"`, `"disabled"` hoặc `"revoked"`)
     * `created_at`: dấu thời gian tạo (chuỗi ISO 8601)
     * `revoked_at`: dấu thời gian thu hồi (chuỗi, hoặc `null` nếu chưa thu hồi)
4. **Thu hồi API key không dùng hoặc bị lộ**:
   * Endpoint: `POST /keys/{key_id}/revoke` (lưu ý: dùng `POST` với `key_id` đích trong đường dẫn; body yêu cầu rỗng)
   * Header: `Authorization: Bearer <token>`
   * Hành vi: lũy đẳng; API key ở trạng thái `active` hoặc `disabled` đều có thể thu hồi. Nếu đã thu hồi, trả HTTP 200 và không thay đổi. Sau khi thu hồi, yêu cầu dùng API key đó bị từ chối.
   * Phản hồi: HTTP 200 trả đối tượng API key đã thu hồi (các trường khớp đối tượng API key ở trên, với `status: "revoked"` và dấu thời gian trong `revoked_at`).

> **Key and secret security**
>
> Lưu khóa riêng của ví và API key trong biến môi trường hoặc trình quản lý bí mật. Không bao giờ commit chúng vào kho mã, ghi vào log hay dán vào cuộc trò chuyện với AI.


## Khuyến nghị bảo mật

* **Dùng API key ngắn hạn và thu hồi khi hoàn tất**: Với tác vụ tự động hoặc tạm thời, tạo API key ngắn hạn bằng `expires_in_secs` và thu hồi ngay qua `POST /keys/{key_id}/revoke` khi hoàn tất công việc.

## Giới hạn tốc độ đăng ký (`signup_rate_limited`)

Việc tạo tài khoản chịu giới hạn tốc độ đăng ký. Token bucket mỗi IP có dung lượng 100 tài khoản và nạp lại 100 tài khoản/giờ cho mỗi địa chỉ IPv4 hoặc tiền tố IPv6 /64, dùng chung cho đăng ký SIWE và OAuth:

* Khi vượt giới hạn đăng ký, `POST /auth/siwe/login` trả HTTP 429 `signup_rate_limited` với header `Retry-After` cho biết số giây cần chờ.
* Trường `reason` phân biệt phạm vi giới hạn:
  * `per_ip`: hạn mức đăng ký của tiền tố IP yêu cầu đã cạn.
  * `global`: giới hạn đăng ký tổng hợp của nền tảng đã cạn.
* Giới hạn tốc độ đăng ký chỉ áp dụng cho đăng ký tài khoản mới. Tài khoản hiện có đăng nhập không bị chặn bởi giới hạn tốc độ đăng ký.

## Tài nguyên liên quan

* Đọc [hướng dẫn tích hợp AI Agent](https://docs.blockvectra.com/vi/guides/ai-agents/) để tìm hiểu MCP server không cần API key và tệp ngữ cảnh máy đọc được.
* Xem [Bắt đầu nhanh](https://docs.blockvectra.com/vi/quickstart/) để lấy ví dụ client bằng nhiều ngôn ngữ.
* Xem [tham chiếu lỗi](https://docs.blockvectra.com/vi/errors/) để biết đầy đủ mã lỗi, reason và cách khôi phục tự động.

## Các bước tiếp theo

* Gửi lệnh gọi JSON-RPC hoặc Data API đầu tiên bằng `x-api-key: $BLOCKVECTRA_API_KEY`.
* [Kiểm tra số dư và giới hạn tài khoản](https://docs.blockvectra.com/vi/guides/ai-agents/#query-balance-get-v1account) bằng `GET /v1/account`.
* Làm theo [hướng dẫn nạp tiền bằng lập trình cho Agent](https://docs.blockvectra.com/vi/guides/agent-topup/) để duy trì số dư.
