# Pendaftaran terprogram: masuk dengan dompet dan buat API key untuk Agent dan CI

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

Untuk AI Agent otonom, pipeline CI, dan skrip otomatis yang berjalan tanpa browser, BlockVectra menyediakan alur masuk dan pembukaan akun secara terprogram berdasarkan tanda tangan dompet Ethereum (EIP-4361 / EIP-191).

> **Key security**
>
> Jangan pernah menempelkan private key, token sesi, atau API key ke percakapan dengan AI atau mengirimkannya sebagai argumen alat MCP.


Sebelum mendaftar, Anda dapat terlebih dahulu mencoba endpoint publik tanpa API key `https://api.blockvectra.com/v1/robinhood_mainnet/public` (hanya metode JSON-RPC dompet, Data API memerlukan API key; metode dan batas mengikuti `/v1/chains`); daftar akun jika kuotanya tidak cukup.

## Gambaran alur kerja

Alur pendaftaran terprogram dan penyediaan API key terdiri dari empat langkah:

1. **Minta challenge**: Kirim permintaan ke `POST /auth/siwe/challenge` untuk mendapatkan pesan masuk yang dibuat server.
2. **Tandatangani pesan**: Tandatangani pesan tersebut persis sebagaimana diterima dengan dompet Ethereum EOA menggunakan EIP-191 (`personal_sign`).
3. **Masuk / buka akun**: Kirim pesan tanpa perubahan beserta tanda tangannya ke `POST /auth/siwe/login`. Saat dompet pertama kali masuk, akun dibuat secara otomatis (`account_created: true`). Akun baru mendapatkan 30,000,000 CU saat pendaftaran — tanpa kartu kredit.
4. **Buat API key**: Gunakan token sesi untuk memanggil `POST /keys` dan membuat API key.

## Contoh lengkap yang dapat dijalankan

Mulai di sini: gunakan penanda tangan Ethereum EOA lokal, buat API key, dan verifikasi dengan eth\_blockNumber.
Untuk contoh Bash, Anda memerlukan curl, jq, dan Foundry cast. Simpan kredensial dompet dalam lingkungan penandatanganan lokal Anda.

Templat awal lengkap: [blockvectra/agent-quickstart](https://github.com/blockvectra/agent-quickstart)

Skrip berikut membaca kredensial dompet, menyelesaikan urutan challenge dan login, menyediakan API key, mengekspor atau mencetak `export BLOCKVECTRA_API_KEY=...` untuk konfigurasi lingkungan, dan mengirim permintaan verifikasi `eth_blockNumber`:

API key baru memerlukan beberapa detik untuk aktif; contoh ini mencoba ulang secara otomatis.

**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 dan mode terprogram

Semua endpoint autentikasi dan pengelolaan API key menggunakan base URL resmi:

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

### Tidak menyertakan header Origin

Permintaan terprogram beroperasi dalam **mode terprogram**:

* Permintaan challenge (`POST /auth/siwe/challenge`) dan login (`POST /auth/siwe/login`) **tidak boleh menyertakan header `Origin`** (`curl` dan klien HTTP standar tidak menyertakan header ini secara default; jangan menambahkannya secara manual).
* Jika header `Origin` dikirim tetapi bukan domain konsol web yang dikonfigurasi (termasuk string kosong atau `null`), permintaan challenge mengembalikan HTTP 400 `invalid_request`.
* Jika mode saat login tidak sesuai dengan mode challenge (misalnya meminta challenge terprogram tanpa `Origin`, lalu mengirim login dengan header `Origin`, atau sebaliknya), permintaan login mengembalikan HTTP 400 `siwe_invalid` dengan `reason: domain_mismatch`.

### Integritas pesan dan persyaratan dompet

* **Tanda tangan dan pengiriman tanpa perubahan**: Klien harus menandatangani dan mengirim teks pesan persis sebagaimana dikembalikan endpoint challenge. Jangan mengubah spasi, domain, chain ID, atau bidang apa pun. Perubahan apa pun menghasilkan HTTP 400 `siwe_invalid` dengan `reason: signature`.
* **Dompet yang didukung**: Externally Owned Accounts (EOA) Ethereum mainnet (Chain ID 1). Tanda tangan harus berupa tanda tangan ECDSA 65 byte (`personal_sign`). Dompet kontrak (EIP-1271) dan smart account tidak didukung.
* **Masa berlaku challenge**: Setiap nonce challenge hanya dapat digunakan sekali dan kedaluwarsa setelah 5 menit.

### Body permintaan dan atribusi pendaftaran (opsional)

Body permintaan `POST /auth/siwe/login` menerima parameter autentikasi wajib dan bidang atribusi pendaftaran opsional:

* **Bidang wajib**:
  * `message`: String pesan SIWE lengkap yang diperoleh dari endpoint challenge.
  * `signature`: Tanda tangan heksadesimal 65 byte (berawalan `0x`) yang dihasilkan dengan menandatangani `message` melalui EIP-191 menggunakan dompet Ethereum.
* **Bidang atribusi opsional** (disimpan hanya sekali saat akun baru dibuat; diabaikan saat login berikutnya):
  * `ref`: Token kanal dalam huruf kecil yang cocok dengan `^[a-z0-9._-]{1,64}$` (huruf ASCII kecil, angka, `.`, `_`, `-`, 1–64 karakter). Misalnya, Agent otonom dapat mengisinya dengan pengenal framework atau runtime mereka (misalnya `my-agent.v1`). Nilai yang tidak sesuai (termasuk huruf besar, string kosong, panjang berlebih, atau karakter yang tidak didukung) mengembalikan HTTP 400 `invalid_request` tanpa konversi huruf besar/kecil dan mencegah pembuatan akun; jangan sertakan atau kirim `null` jika tidak berlaku.
  * `referrer`: String URL atau hostname sumber; hanya tipe selain string yang mengembalikan HTTP 400.

Mengirim bidang yang tidak didefinisikan seperti `signup_method` mengembalikan HTTP 400 `invalid_request`.

## Token sesi dan API key

### Siklus hidup token sesi

* **Format**: `rgs_` diikuti 64 karakter heksadesimal huruf kecil.
* **Masa berlaku**: Masa hidup absolut 7 hari; kedaluwarsa otomatis setelah 24 jam tanpa aktivitas.
* **Tanpa refresh token**: Saat token sesi kedaluwarsa, mulai alur challenge dan login baru.
* **Header**: Kirim token sesi dalam header permintaan `Authorization: Bearer rgs_...`.

### Pembuatan API key

* Panggil `POST /keys` dengan token sesi untuk membuat API key (`rgw_` diikuti 64 karakter heksadesimal).
* Per akun, maksimal 20 API key yang belum dicabut dan belum kedaluwarsa (`active` + `disabled`); API key yang kedaluwarsa tidak dihitung. Melampaui batas ini mengembalikan HTTP 409 `key_limit_reached` dengan `reason: active_keys` dan `limit: 20`; cabut satu API key terlebih dahulu. Batas ini berlaku di semua identitas, sesi, dan chain akun. Pembuatan dan rotasi API key juga dibatasi hingga 20 per 24 jam; melampaui batas ini mengembalikan HTTP 429 `rate_limited` dengan `Retry-After: 3600`.
* Batas dan kedaluwarsa opsional: Anda dapat menyediakan `cu_cap` (batas CU selama masa hidup API key, yang merupakan batas lunak) dan kedaluwarsa (`expires_in_secs` atau `expires_at`, hingga jumlah hari maksimum yang diizinkan kebijakan API key); setelah kedaluwarsa atau batas habis, server mengembalikan 403 (JSON-RPC `-32025`, alasan `key_expired` atau `key_cap_exhausted`).
* Secret `api_key` **hanya dikembalikan sekali saat pembuatan**. Segera simpan dengan aman dalam pengelola secret atau variabel lingkungan.
* Satu API key berlaku di semua chain yang didukung pada JSON-RPC dan Data API.

## Kehilangan sesi atau API key?

Di BlockVectra, **identitas akun Agent terikat pada alamat dompet Ethereum yang digunakan saat pendaftaran**. Jika token sesi kedaluwarsa atau API key hilang atau bocor, Anda dapat memulihkan kendali penuh hanya menggunakan dompet tersebut:

1. **Autentikasi ulang dengan dompet yang sama**: Minta challenge, tandatangani dengan dompet yang sama, lalu kirim permintaan login (`POST /auth/siwe/login`). Server memverifikasi tanda tangan, masuk ke akun yang sudah ada dengan `account_created: false`, dan menerbitkan token sesi baru.
2. **Buat API key baru**: Dengan token sesi baru, panggil `POST /keys` dengan `{"label": "..."}` dan header `Authorization: Bearer <token>`. Endpoint mengembalikan HTTP 201 dengan detail API key yang dibuat dalam `key` dan secret sekali tampil dalam `api_key`. Segera simpan API key ini dalam variabel lingkungan atau pengelola secret.
3. **Daftar semua API key akun**:
   * Endpoint: `GET /keys`
   * Header: `Authorization: Bearer <token>`
   * Parameter kueri: `include_revoked=true` opsional (saat `true`, menyertakan API key yang dicabut; secara default hanya API key aktif/dinonaktifkan).
   * Respons: HTTP 200 dengan JSON `{"items": [...]}`. Setiap elemen dalam array `items` mencakup:
     * `key_id`: pengenal unik API key (string)
     * `label`: label API key (string atau `null`)
     * `status`: status (`"active"`, `"disabled"`, atau `"revoked"`)
     * `created_at`: waktu pembuatan (string ISO 8601)
     * `revoked_at`: waktu pencabutan (string, atau `null` jika belum dicabut)
4. **Cabut API key yang tidak digunakan atau bocor**:
   * Endpoint: `POST /keys/{key_id}/revoke` (catatan: menggunakan `POST` dengan `key_id` tujuan pada jalur; body permintaan kosong)
   * Header: `Authorization: Bearer <token>`
   * Perilaku: idempoten; API key berstatus `active` maupun `disabled` dapat dicabut. Jika sudah dicabut, mengembalikan HTTP 200 tanpa perubahan. Setelah pencabutan, permintaan menggunakan API key tersebut ditolak.
   * Respons: HTTP 200 yang mengembalikan objek API key yang dicabut (bidang sesuai objek API key di atas, dengan `status: "revoked"` dan waktu pada `revoked_at`).

> **Key and secret security**
>
> Simpan private key dompet dan API key dalam variabel lingkungan atau pengelola secret. Jangan pernah menyimpannya dalam commit repositori kode, menuliskannya ke log, atau menempelkannya ke percakapan AI.


## Rekomendasi keamanan

* **Gunakan API key berumur pendek dan cabut setelah selesai**: Untuk tugas otomatis atau sementara, buat API key berumur pendek dengan `expires_in_secs` dan segera cabut melalui `POST /keys/{key_id}/revoke` setelah pekerjaan selesai.

## Batas laju pendaftaran (`signup_rate_limited`)

Pembuatan akun tunduk pada batas laju pendaftaran. Token bucket per IP memiliki kapasitas 100 akun dan terisi ulang dengan laju 100 akun/jam per alamat IPv4 atau prefiks IPv6 /64, digunakan bersama oleh pendaftaran SIWE dan OAuth:

* Saat melampaui batas pendaftaran, `POST /auth/siwe/login` mengembalikan HTTP 429 `signup_rate_limited` dengan header `Retry-After` yang menunjukkan jumlah detik untuk menunggu.
* Bidang `reason` membedakan cakupan batas:
  * `per_ip`: jatah pendaftaran untuk prefiks IP peminta telah habis.
  * `global`: batas agregat pendaftaran platform telah habis.
* Batas laju pendaftaran hanya mengevaluasi pendaftaran akun baru. Login akun yang sudah ada tidak diblokir oleh batas laju pendaftaran.

## Sumber daya terkait

* Baca [panduan integrasi AI Agent](https://docs.blockvectra.com/id/guides/ai-agents/) untuk mempelajari server MCP tanpa API key dan berkas konteks yang dapat dibaca mesin.
* Tinjau [Panduan Cepat](https://docs.blockvectra.com/id/quickstart/) untuk contoh klien dalam berbagai bahasa pemrograman.
* Periksa [referensi error](https://docs.blockvectra.com/id/errors/) untuk kode error lengkap, alasan, dan tindakan pemulihan otomatis.

## Langkah berikutnya

* Kirim panggilan JSON-RPC atau Data API pertama Anda dengan `x-api-key: $BLOCKVECTRA_API_KEY`.
* [Periksa saldo akun dan batas](https://docs.blockvectra.com/id/guides/ai-agents/#kueri-saldo-get-v1account) menggunakan `GET /v1/account`.
* Ikuti [panduan top up terprogram Agent](https://docs.blockvectra.com/id/guides/agent-topup/) untuk menjaga saldo.
