# Inscription programmatique : connexion par portefeuille et création d'API key pour agents et CI

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

Pour les agents IA autonomes, les pipelines CI et les scripts automatisés s'exécutant sans navigateur, BlockVectra fournit un flux de connexion et d'ouverture de compte programmatique basé sur les signatures de portefeuille Ethereum (EIP-4361 / EIP-191).

> **Sécurité de la clé**
>
> Ne collez jamais de clés privées, de jetons de session ou d'API keys dans des conversations avec l'IA et ne les transmettez pas en tant qu'arguments d'outils MCP.


Avant de vous inscrire, vous pouvez d'abord tester le point de terminaison public sans clé `https://api.blockvectra.com/v1/robinhood_mainnet/public` (méthodes JSON-RPC de portefeuille uniquement, la Data API nécessite une clé ; les méthodes et limites dépendent de `/v1/chains`) ; créez un compte si le quota est insuffisant.

## Aperçu du flux de travail

Le flux d'inscription programmatique et d'attribution de clés se compose de quatre étapes :

1. **Demander un challenge** : envoyez une requête à `POST /auth/siwe/challenge` pour obtenir un message de connexion généré par le serveur.
2. **Signer le message** : signez le message exact avec un portefeuille Ethereum EOA en utilisant EIP-191 (`personal_sign`).
3. **Se connecter / ouvrir un compte** : soumettez le message textuel et la signature à `POST /auth/siwe/login`. Lors de la première connexion pour un portefeuille, un compte est automatiquement créé (`account_created: true`). Les nouveaux comptes reçoivent 30,000,000 CU à l'inscription — aucune carte bancaire requise.
4. **Créer une API key** : utilisez le jeton de session pour appeler `POST /keys` et créer une API key.

## Exemples complets exécutables

Commencez ici : utilisez un signataire Ethereum EOA local, créez une clé et vérifiez-la avec eth\_blockNumber.
Pour l'exemple Bash, vous avez besoin de curl, jq et Foundry cast. Conservez les identifiants de portefeuille dans votre environnement de signature local.

Modèle de démarrage complet : [blockvectra/agent-quickstart](https://github.com/blockvectra/agent-quickstart)

Les scripts suivants lisent les identifiants de portefeuille, effectuent la séquence de challenge et de connexion, approvisionnent une API key, exportent ou affichent `export BLOCKVECTRA_API_KEY=...` pour la configuration de l'environnement, et envoient une requête de vérification `eth_blockNumber` :

Les nouvelles clés prennent quelques secondes pour s'activer ; ces exemples réessaient automatiquement.

**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 de base et mode programmatique

Tous les points de terminaison d'authentification et de gestion des clés utilisent l'URL de base officielle :

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

### Omission de l'en-tête Origin

Les requêtes programmatiques fonctionnent en **mode programmatique** :

* Les requêtes de challenge (`POST /auth/siwe/challenge`) et de connexion (`POST /auth/siwe/login`) **ne doivent pas inclure l'en-tête `Origin`** (`curl` et les clients HTTP standard omettent cet en-tête par défaut ; ne l'ajoutez pas manuellement).
* Si un en-tête `Origin` est envoyé sans correspondre à un domaine configuré de la console web (y compris une chaîne vide ou `null`), la requête de challenge renvoie HTTP 400 `invalid_request`.
* Si le mode lors de la connexion ne correspond pas au mode du challenge (par exemple, demander un challenge programmatique sans `Origin` puis soumettre la connexion avec un en-tête `Origin`, ou inversement), la requête de connexion renvoie HTTP 400 `siwe_invalid` avec `reason: domain_mismatch`.

### Intégrité des messages et prérequis de portefeuille

* **Signature et soumission textuelles** : les clients doivent signer et soumettre le texte du message exactement tel qu'il a été renvoyé par le point de terminaison de challenge. Ne modifiez ni les espaces, ni le domaine, ni l'ID de chaîne, ni aucun autre champ. Toute modification entraîne une réponse HTTP 400 `siwe_invalid` avec `reason: signature`.
* **Portefeuilles pris en charge** : comptes de type Externally Owned Accounts (EOA) sur Ethereum mainnet (Chain ID 1). La signature doit être une signature ECDSA de 65 octets (`personal_sign`). Les portefeuilles de contrats (EIP-1271) et les smart accounts ne sont pas pris en charge.
* **Validité du challenge** : chaque nonce de challenge est à usage unique et expire au bout de 5 minutes.

### Corps de requête et attribution d'inscription (facultatif)

Le corps de la requête `POST /auth/siwe/login` accepte des paramètres d'authentification obligatoires et des champs d'attribution d'inscription facultatifs :

* **Champs obligatoires** :
  * `message` : la chaîne complète du message SIWE obtenue à partir du point de terminaison de challenge.
  * `signature` : la signature hexadécimale de 65 octets (préfixée par `0x`) produite en signant `message` via EIP-191 avec un portefeuille Ethereum.
* **Champs d'attribution facultatifs** (enregistrés une seule fois lors de la création d'un nouveau compte ; ignorés lors des connexions ultérieures) :
  * `ref` : un jeton de canal en minuscules respectant `^[a-z0-9._-]{1,64}$` (lettres ASCII minuscules, chiffres, `.`, `_`, `-`, 1 à 64 caractères). Par exemple, les agents autonomes peuvent renseigner l'identifiant de leur framework ou environnement d'exécution (par exemple `my-agent.v1`). Les valeurs non conformes (notamment majuscules, chaînes vides, longueur excessive ou caractères non pris en charge) renvoient HTTP 400 `invalid_request` sans conversion de casse et empêchent la création du compte ; omettez ce champ ou transmettez `null` s'il n'est pas applicable.
  * `referrer` : une URL source ou une chaîne d'hôte ; seuls les types autres que chaîne renvoient HTTP 400.

L'envoi de champs non définis tels que `signup_method` renvoie HTTP 400 `invalid_request`.

## Jetons de session et API keys

### Cycle de vie des jetons de session

* **Format** : `rgs_` suivi de 64 caractères hexadécimaux en minuscules.
* **Validité** : durée de vie absolue de 7 jours ; expire automatiquement après 24 heures d'inactivité.
* **Aucun jeton de rafraîchissement** : lorsqu'un jeton de session expire, initiez un nouveau flux de challenge et de connexion.
* **En-tête** : transmettez le jeton de session dans l'en-tête de requête `Authorization: Bearer rgs_...`.

### Création d'API key

* Appelez `POST /keys` avec le jeton de session pour créer une API key (`rgw_` suivi de 64 caractères hexadécimaux).
* Par compte, au maximum 20 clés non révoquées et non expirées (`active` + `disabled`) ; les clés expirées ne sont pas comptabilisées. Tout dépassement renvoie HTTP 409 `key_limit_reached` avec `reason: active_keys` et `limit: 20` ; révoquez d'abord une clé. Ce plafond s'applique à l'ensemble des identités, sessions et chaînes du compte. La création et la rotation de clés sont également limitées à 20 par période de 24 heures ; tout dépassement renvoie HTTP 429 `rate_limited` avec `Retry-After: 3600`.
* Plafond et expiration facultatifs : vous pouvez définir `cu_cap` (plafond de CU sur la durée de vie de la clé, correspondant à un plafond indicatif / soft cap) et une expiration (`expires_in_secs` ou `expires_at`, jusqu'au nombre maximal de jours autorisé par la politique des clés) ; une fois la clé expirée ou le plafond atteint, le serveur renvoie 403 (JSON-RPC `-32025`, motif `key_expired` ou `key_cap_exhausted`).
* Le secret `api_key` est **renvoyé une seule fois lors de la création**. Stockez-le immédiatement en lieu sûr dans votre gestionnaire de secrets ou vos variables d'environnement.
* Une seule API key fonctionne sur toutes les chaînes prises en charge pour JSON-RPC et la Data API.

## Session ou API key perdue ?

Sur BlockVectra, **l'identité du compte d'un agent est liée à l'adresse de portefeuille Ethereum utilisée lors de l'inscription**. Si votre jeton de session expire ou si une API key est perdue ou divulguée, vous pouvez en récupérer le contrôle total en utilisant uniquement ce portefeuille :

1. **Se réauthentifier avec le même portefeuille** : demandez un challenge, signez-le avec le même portefeuille et soumettez la requête de connexion (`POST /auth/siwe/login`). Le serveur vérifie la signature, se connecte au compte existant avec `account_created: false` et délivre un nouveau jeton de session.
2. **Créer une nouvelle API key** : avec le nouveau jeton de session, appelez `POST /keys` avec `{"label": "..."}` et l'en-tête `Authorization: Bearer <token>`. Le point de terminaison renvoie HTTP 201 avec les détails de la clé créée dans `key` et le secret à usage unique dans `api_key`. Enregistrez immédiatement cette clé dans vos variables d'environnement ou votre gestionnaire de secrets.
3. **Lister toutes les clés du compte** :
   * Point de terminaison : `GET /keys`
   * En-tête : `Authorization: Bearer <token>`
   * Paramètre de requête : `include_revoked=true` facultatif (si `true`, inclut les clés révoquées ; par défaut, ne liste que les clés actives ou désactivées).
   * Réponse : HTTP 200 avec le JSON `{"items": [...]}`. Chaque élément du tableau `items` comprend :
     * `key_id` : identifiant unique de clé (chaîne)
     * `label` : libellé de clé (chaîne ou `null`)
     * `status` : statut (`"active"`, `"disabled"` ou `"revoked"`)
     * `created_at` : horodatage de création (chaîne ISO 8601)
     * `revoked_at` : horodatage de révocation (chaîne, ou `null` si non révoquée)
4. **Révoquer les clés inutilisées ou compromises** :
   * Point de terminaison : `POST /keys/{key_id}/revoke` (remarque : utilise `POST` avec le `key_id` cible dans le chemin ; corps de requête vide)
   * En-tête : `Authorization: Bearer <token>`
   * Comportement : idempotent ; les clés ayant le statut `active` ou `disabled` peuvent être révoquées. Si elle est déjà révoquée, la requête renvoie HTTP 200 à l'identique. Après révocation, les requêtes utilisant cette clé sont rejetées.
   * Réponse : HTTP 200 renvoyant l'objet de clé révoquée (les champs correspondent à l'objet de clé ci-dessus, avec `status: "revoked"` et un horodatage dans `revoked_at`).

> **Sécurité des clés et des secrets**
>
> Stockez les clés privées de portefeuille et les API keys dans des variables d'environnement ou un gestionnaire de secrets. Ne les commitez jamais dans des dépôts de code, ne les écrivez pas dans des logs et ne les collez pas dans des conversations avec l'IA.


## Recommandations de sécurité

* **Utiliser des clés à court terme et les révoquer une fois terminé** : pour les tâches automatisées ou éphémères, créez des clés à courte durée de vie avec `expires_in_secs` et révoquez-les immédiatement via `POST /keys/{key_id}/revoke` dès la fin du travail.

## Limites de débit à l'inscription (`signup_rate_limited`)

La création de compte est soumise à des limites de débit d'inscription. Le seau à jetons par IP dispose d'une capacité de 100 comptes et se recharge à raison de 100 comptes/heure par adresse IPv4 ou préfixe IPv6 /64, partagé entre les inscriptions SIWE et OAuth :

* En cas de dépassement des limites d'inscription, `POST /auth/siwe/login` renvoie HTTP 429 `signup_rate_limited` avec un en-tête `Retry-After` indiquant le nombre de secondes à attendre.
* Le champ `reason` distingue la portée de la limite :
  * `per_ip` : le budget d'inscription pour le préfixe IP demandeur a été épuisé.
  * `global` : la limite globale d'inscription de la plateforme a été atteinte.
* Les limites de débit d'inscription n'évaluent que les nouveaux enregistrements de comptes. La connexion de comptes existants n'est pas bloquée par ces limites.

## Ressources associées

* Lisez le [guide d'intégration des agents IA](https://docs.blockvectra.com/fr/guides/ai-agents/) pour découvrir le serveur MCP sans clé et les fichiers de contexte lisibles par machine.
* Consultez le [Démarrage rapide](https://docs.blockvectra.com/fr/quickstart/) pour des exemples de clients en plusieurs langages.
* Consultez la [référence des erreurs](https://docs.blockvectra.com/fr/errors/) pour la liste complète des codes d'erreur, des motifs et des actions de récupération automatisées.

## Prochaines étapes

* Envoyez votre premier appel JSON-RPC ou Data API avec `x-api-key: $BLOCKVECTRA_API_KEY`.
* [Vérifier le solde et les limites du compte](https://docs.blockvectra.com/fr/guides/ai-agents/#query-balance-get-v1account) à l'aide de `GET /v1/account`.
* Suivre le [guide de recharge programmatique pour agents](https://docs.blockvectra.com/fr/guides/agent-topup/) pour maintenir le solde.
