# Программная регистрация: вход по подписи кошелька и создание API key для агентов и CI

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

Для автономных ИИ-агентов, конвейеров CI и автоматизированных скриптов, работающих без браузера, BlockVectra предоставляет процесс программного входа и создания аккаунта на основе подписи кошелька Ethereum (EIP-4361 / EIP-191).

> **Безопасность ключей**
>
> Никогда не вставляйте приватные ключи, токены сессий или API keys в диалоги с ИИ и не передавайте их в качестве аргументов инструментов MCP.


Перед регистрацией вы можете сначала опробовать публичный эндпоинт без ключа `https://api.blockvectra.com/v1/robinhood_mainnet/public` (только методы кошелька JSON-RPC, Data API требует ключ; методы и лимиты определяются `/v1/chains`); зарегистрируйте аккаунт, если этой квоты недостаточно.

## Обзор рабочего процесса

Процесс программной регистрации и выпуска ключа состоит из четырех шагов:

1. **Запрос вызова (challenge)**: отправьте запрос к `POST /auth/siwe/challenge`, чтобы получить сгенерированное сервером сообщение для входа.
2. **Подписание сообщения**: подпишите точный текст сообщения с помощью EOA-кошелька Ethereum по стандарту EIP-191 (`personal_sign`).
3. **Вход / открытие аккаунта**: отправьте исходное сообщение и подпись в `POST /auth/siwe/login`. При первом входе с помощью кошелька аккаунт создается автоматически (`account_created: true`). Новые аккаунты получают 30,000,000 CU при регистрации — банковская карта не требуется.
4. **Создание API key**: используйте токен сессии для вызова `POST /keys` и создания API key.

## Полные рабочие примеры

Начните здесь: используйте локальный механизм подписи EOA Ethereum, создайте ключ и проверьте его с помощью eth\_blockNumber.
Для примера на Bash вам понадобятся curl, jq и cast из Foundry. Храните учетные данные кошелька в локальном окружении для подписи.

Полный начальный шаблон: [blockvectra/agent-quickstart](https://github.com/blockvectra/agent-quickstart)

Следующие скрипты считывают учетные данные кошелька, выполняют последовательность запроса вызова и входа, создают 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
```


## Базовый URL и программный режим

Все эндпоинты аутентификации и управления ключами используют официальный базовый URL:

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

### Исключение заголовка Origin

Программные запросы работают в **программном режиме**:

* Запросы на получение вызова (`POST /auth/siwe/challenge`) и вход (`POST /auth/siwe/login`) **не должны содержать заголовок `Origin`** (`curl` и стандартные HTTP-клиенты опускают этот заголовок по умолчанию; не добавляйте его вручную).
* Если заголовок `Origin` передан, но не соответствует настроенному домену веб-консоли (включая пустую строку или `null`), запрос вызова возвращает HTTP 400 `invalid_request`.
* Если режим при входе не совпадает с режимом вызова (например, запрос программного вызова без `Origin`, а затем отправка входа с заголовком `Origin`, или наоборот), запрос входа возвращает HTTP 400 `siwe_invalid` с причиной `reason: domain_mismatch`.

### Целостность сообщения и требования к кошельку

* **Подписание и отправка без изменений**: клиенты должны подписывать и отправлять текст сообщения в точности в том виде, в каком он возвращен эндпоинтом вызова. Не изменяйте пробелы, домен, chain ID или любые другие поля. Любая модификация приводит к ошибке HTTP 400 `siwe_invalid` с причиной `reason: signature`.
* **Поддерживаемые кошельки**: Externally Owned Accounts (EOA) в Ethereum mainnet (Chain ID 1). Подпись должна представлять собой 65-байтную подпись ECDSA (`personal_sign`). Контрактные кошельки (EIP-1271) и смарт-аккаунты не поддерживаются.
* **Срок действия вызова**: каждый одноразовый номер (nonce) вызова предназначен для однократного использования и истекает через 5 минут.

### Тело запроса и атрибуция регистрации (необязательно)

Тело запроса `POST /auth/siwe/login` принимает обязательные параметры аутентификации и необязательные поля атрибуции регистрации:

* **Обязательные поля**:
  * `message`: полная строка сообщения SIWE, полученная из эндпоинта вызова.
  * `signature`: 65-байтная шестнадцатеричная подпись (с префиксом `0x`), полученная путем подписания `message` по стандарту EIP-191 с помощью кошелька Ethereum.
* **Необязательные поля атрибуции** (сохраняются только один раз при создании нового аккаунта; игнорируются при последующих входах):
  * `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 keys

### Жизненный цикл токена сессии

* **Формат**: префикс `rgs_`, за которым следуют 64 шестнадцатеричных символа в нижнем регистре.
* **Срок действия**: абсолютный срок действия 7 дней; автоматически истекает после 24 часов неактивности.
* **Без токена обновления**: когда срок действия токена сессии истекает, начните новый процесс получения вызова и входа.
* **Заголовок**: передавайте токен сессии в заголовке запроса `Authorization: Bearer rgs_...`.

### Создание API key

* Вызовите `POST /keys` с токеном сессии, чтобы создать API key (`rgw_`, за которым следуют 64 шестнадцатеричных символа).
* На один аккаунт допускается не более 20 не отозванных и не истекших (`active` + `disabled`) ключей; истекшие ключи не учитываются. Превышение этого лимита возвращает HTTP 409 `key_limit_reached` с причиной `reason: active_keys` и лимитом `limit: 20`; сначала отзовите неиспользуемый ключ. Это ограничение действует для всех идентификаторов, сессий и сетей аккаунта. Создание и ротация ключей также ограничены 20 операциями за 24 часа; превышение этого лимита возвращает 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` **возвращается только один раз при создании**. Немедленно сохраните его в надежном месте в вашем менеджере секретов или переменных окружения.
* Один API key работает во всех поддерживаемых сетях для JSON-RPC и Data API.

## Потеряли сессию или API key?

В BlockVectra **идентификатор аккаунта агента привязан к адресу кошелька Ethereum, использованному при регистрации**. Если срок действия токена сессии истек или API key был утерян либо скомпрометирован, вы можете восстановить полный контроль, используя только этот кошелек:

1. **Повторная аутентификация с тем же кошельком**: запросите вызов, подпишите его тем же кошельком и отправьте запрос на вход (`POST /auth/siwe/login`). Сервер проверит подпись, выполнит вход в существующий аккаунт с `account_created: false` и выдаст новый токен сессии.
2. **Создание нового API key**: с новым токеном сессии вызовите `POST /keys` с `{"label": "..."}` и заголовком `Authorization: Bearer <token>`. Эндпоинт вернет HTTP 201 с данными созданного ключа в `key` и однократным секретом в `api_key`. Немедленно сохраните этот ключ в ваших переменных окружения или менеджере секретов.
3. **Список всех ключей аккаунта**:
   * Эндпоинт: `GET /keys`
   * Заголовок: `Authorization: Bearer <token>`
   * Параметр запроса: необязательный `include_revoked=true` (при значении `true` включает отозванные ключи; по умолчанию только активные/отключенные ключи).
   * Ответ: 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` (обратите внимание: используется метод `POST` с целевым `key_id` в пути; пустое тело запроса)
   * Заголовок: `Authorization: Bearer <token>`
   * Поведение: идемпотентно; могут быть отозваны ключи со статусом как `active`, так и `disabled`. Если ключ уже отозван, возвращается HTTP 200 без изменений. После отзыва запросы с этим ключом отклоняются.
   * Ответ: HTTP 200 с возвратом объекта отозванного ключа (поля совпадают с объектом ключа выше, со значением `status: "revoked"` и временной меткой в `revoked_at`).

> **Безопасность ключей и секретов**
>
> Храните приватные ключи кошельков и API keys в переменных окружения или менеджере секретов. Никогда не фиксируйте их в репозиториях кода, не записывайте в логи и не вставляйте в чаты с ИИ.


## Рекомендации по безопасности

* **Используйте краткосрочные ключи и отзывайте их по завершении работы**: для автоматизированных или временных задач создавайте короткоживущие ключи с `expires_in_secs` и немедленно отзывайте их через `POST /keys/{key_id}/revoke` после завершения работы.

## Ограничения частоты регистрации (`signup_rate_limited`)

Создание аккаунтов подчиняется ограничениям частоты регистрации. Корзина токенов на IP имеет емкость 100 аккаунтов и пополняется со скоростью 100 аккаунтов в час на один IPv4-адрес или префикс IPv6 /64, разделяемый между регистрацией через SIWE и OAuth:

* При превышении лимитов регистрации `POST /auth/siwe/login` возвращает HTTP 429 `signup_rate_limited` с заголовком `Retry-After`, указывающим количество секунд ожидания.
* Поле `reason` указывает область действия лимита:
  * `per_ip`: исчерпан лимит регистрации для префикса IP, с которого поступил запрос.
  * `global`: исчерпан общий лимит регистрации на платформе.
* Ограничения частоты регистрации оценивают только создание новых аккаунтов. Вход в существующие аккаунты не блокируется лимитами регистрации.

## Связанные ресурсы

* Ознакомьтесь с [руководством по интеграции ИИ-агентов](https://docs.blockvectra.com/en/guides/ai-agents/), чтобы узнать о сервере MCP без ключа и машиночитаемых файлах контекста.
* Изучите [Быстрый старт](https://docs.blockvectra.com/en/quickstart/) с примерами клиентов на разных языках.
* Изучите [Справочник по ошибкам](https://docs.blockvectra.com/en/errors/) с полным списком кодов ошибок, причин и автоматизированных действий по восстановлению.

## Следующие шаги

* Отправьте свой первый вызов JSON-RPC или Data API с заголовком `x-api-key: $BLOCKVECTRA_API_KEY`.
* [Проверьте баланс и лимиты аккаунта](https://docs.blockvectra.com/en/guides/ai-agents/#query-balance-get-v1account) с помощью `GET /v1/account`.
* Следуйте [руководству по программному пополнению для агентов](https://docs.blockvectra.com/en/guides/agent-topup/) для поддержания баланса.
