# Annexe B · API

Contrat complet de l'API du serveur Vivra : routes, entrées, sorties et erreurs. Il complète la liste de routes de la page Analyse des projets.

## Généralités

- Base : `https://api.<domaine-vivra>/v1`. JSON partout, en UTF-8.
- Montants on-chain renvoyés en chaîne de caractères (unités brutes) ; montants en USD en nombre à 2 décimales ; dates en ISO 8601 UTC.
- Pagination par curseur : `?limit=50&cursor=<opaque>`, réponse `{ "items": [...], "next_cursor": "..." | null }`. `limit` max 100.
- Auth des votants : cookie `vivra_session` (HttpOnly, Secure, SameSite=Lax), valeur = id de session. Pas de jeton dans le stockage du navigateur.
- Auth du back-office : signature d'un message par un wallet de `team_wallets` (SIWE, EIP-4361), puis cookie `vivra_admin` de 12 h.
- CORS : seulement le domaine de la dapp et celui du back-office.

### Format d'erreur

```json
{ "error": { "code": "vote_closed", "message": "Voting for cycle 4 closed at 2026-11-12T18:00:00Z." } }
```

| HTTP | `code` | Quand |
| --- | --- | --- |
| 400 | `invalid_input` | champ manquant ou mal formé (détail dans `message`) |
| 401 | `unauthenticated` | pas de session valide |
| 403 | `forbidden` | compte de l'équipe, compte bloqué, wallet hors liste blanche |
| 403 | `account_too_new` | compte plus récent que `min_account_age_hours` |
| 404 | `not_found` | ressource inconnue |
| 409 | `vote_closed` / `vote_not_open` | hors fenêtre de vote |
| 422 | `disposable_email` | domaine d'email jetable |
| 422 | `invalid_code` | code faux ou expiré |
| 429 | `rate_limited` | trop de requêtes ; en-tête `Retry-After` |
| 500 | `internal` | erreur serveur, avec un `request_id` |

### Limites de débit

| Route | Limite |
| --- | --- |
| `POST /auth/email` | 3 par email par heure, 10 par IP par heure |
| `POST /auth/verify` | 5 essais par code, puis le code est invalidé |
| `POST /votes`, `POST /supports` | 20 par session par minute |
| Routes publiques en lecture | 120 par IP par minute |

## Routes publiques (sans session)

### `GET /stats`

```json
{ "takeovers": 4, "revived": 2, "verdicts": 3,
  "vivra_burned": "1250000000000000000000000", "cto_fees_received_usd": 4210.55 }
```

### `GET /tokens/:address`

Seulement pour un token publié (candidat d'un cycle publié ou token repris) ; 404 sinon. Renvoie l'en-tête du token, le dernier instantané, les vérifications de contrat, les socials, le dernier score de reprise avec son détail, et l'historique quotidien sur 90 jours (`history: [{ "date", "price_usd", "volume_usd", "holders" }]`).

### `GET /cycles/current`

```json
{ "id": 4, "number": 4, "status": "voting",
  "vote_opens_at": "2026-11-10T18:00:00Z", "vote_closes_at": "2026-11-12T18:00:00Z",
  "voters": 3812,
  "candidates": [ {
    "id": 17, "position": 1, "token": "0x…", "ticker": "GRANDPA",
    "tagline": "…", "thesis_short": "…", "votes": 1290, "share_pct": 33.8,
    "supports": 412, "recovery_score": 68 } ],
  "my_vote": 17 }
```

`my_vote` vaut `null` sans session ou sans vote.

### `GET /candidates/:id`

La fiche complète : tous les blocs de « Contenu d'une fiche candidat », plus `recovery_score` (note figée à la publication et son détail) et `recovery_score_live`.

### `GET /cycles` et `GET /cycles/:id`

Historique des cycles, avec le gagnant et les résultats finaux.

### `GET /operations/:id`

```json
{ "id": 4, "token": "0x…", "ticker": "GRANDPA", "status": "running", "j0": "…",
  "steps": [ { "key": "designer.directions", "agent": "designer", "due_at": "…", "status": "done" } ],
  "spent_usd": 38.12,
  "buyback": { "envelope_wei": "…", "cap_wei": "…", "slices_done": 9, "slices_total": 23, "burned": "…" },
  "links": { "site": "https://grandpa.<domaine-vivra>", "x": "https://x.com/…", "telegram": "https://t.me/…" },
  "cto_request": { "status": "submitted" },
  "verdict": null }
```

### `GET /operations/:id/log`

Pagination par curseur. Chaque ligne : `{ "id", "at", "agent", "kind", "summary", "cost_usd" }`. Jamais `payload`.

### `GET /ledger`

Paramètres : `type`, `from`, `to`, `limit`, `cursor`. Chaque ligne : type, jeton, montant brut et en USD, wallets source et destination, hash, date.

### `GET /wallets`

Les adresses publiques de tous les wallets de la table `wallets`, avec leur solde courant.

## Connexion

### `POST /auth/email`

Entrée : `{ "email": "a@b.com", "captcha_token": "…" }`. Normalise l'email (minuscules, sans espaces), refuse les domaines jetables, crée ou retrouve l'utilisateur, envoie un email contenant un code à 6 chiffres et un lien magique (`/auth/magic?token=…`). Réponse `204`, toujours la même, que l'email existe ou non.

### `POST /auth/verify`

Entrée : `{ "email": "a@b.com", "code": "482913" }` ou `{ "magic_token": "…" }`. Crée la session (30 jours) et pose le cookie. Réponse `{ "user": { "id": 812, "created_at": "…", "can_vote_at": "…" } }`.

### `POST /auth/logout`

Révoque la session courante. `204`.

### `GET /me`

`{ "id", "created_at", "can_vote_at", "votes": [{ "cycle_id", "candidate_id" }], "supports": [candidate_id] }`.

## Vote et soutien (session requise)

### `POST /votes`

Entrée : `{ "candidate_id": 17 }`. Crée ou remplace le vote de l'utilisateur pour le cycle de ce candidat. Erreurs : `vote_not_open`, `vote_closed`, `account_too_new`, `forbidden`. Réponse : le même objet que `GET /cycles/current`, à jour.

### `POST /supports` et `DELETE /supports/:candidate_id`

Entrée : `{ "candidate_id": 17 }`. Idempotent. Réponse `{ "supports": 413 }`.

## Temps réel (WebSocket)

Connexion : `wss://api.<domaine-vivra>/v1/ws`. Le client envoie `{ "subscribe": "cycle:4" }` ou `{ "subscribe": "operation:4" }`. Messages reçus :

| `type` | Canal | Contenu |
| --- | --- | --- |
| `vote.tally` | `cycle:*` | `{ candidates: [{ id, votes, share_pct }], voters }`, au plus 1 par seconde |
| `step.updated` | `operation:*` | `{ key, status, at }` |
| `log.appended` | `operation:*` | une ligne du journal public |
| `slice.executed` | `operation:*` | `{ index, amount_in_wei, amount_burned, tx_hash, at }` |
| `trade` | `operation:*` | `{ price_usd, amount_usd, side, at }` pour le chart live |
| `verdict` | `operation:*` | `{ verdict, at }` |

Ping toutes les 25 s, reconnexion automatique côté client avec reprise depuis le dernier `id` reçu (`{ "subscribe": "operation:4", "since": 1532 }`).

## Back-office (préfixe `/admin`, session admin requise)

| Méthode et route | Rôle |
| --- | --- |
| `POST /admin/auth/nonce`, `POST /admin/auth/verify` | connexion SIWE d'un wallet de l'équipe |
| `POST /admin/projects` | soumettre une adresse (et des liens optionnels) ; lance l'analyse du Scanner |
| `GET /admin/projects?status=analyzed` | projets analysés, avec cible, sécurité et score de reprise |
| `POST /admin/projects/:address/review` | `{ "decision": "validate" \| "reject" \| "rerun", "reason": "…" }` |
| `POST /admin/cycles` | créer un cycle (numéro, horaires, délai minimal du compte) |
| `PUT /admin/cycles/:id/candidates` | fixer les 5 candidats et leurs textes ; refusé si le cycle est publié |
| `POST /admin/candidates/:id/prereport` | lancer le pré-rapport du Scanner |
| `POST /admin/cycles/:id/publish` | publier les 5 fiches d'un coup ; fige les scores |
| `GET /admin/validations?status=pending` | file de validation |
| `POST /admin/validations/:id` | `{ "decision": "approve" \| "reject", "reason": "…" }` |
| `POST /admin/operations/:id/pause`, `/resume`, `/stop` | pilotage et arrêt d'urgence d'une opération |
| `POST /admin/buyback/stop` | arrêt d'urgence de toutes les tranches |
| `PUT /admin/cto-requests/:id` | mettre à jour le statut de la demande Pons |
| `GET /admin/settings`, `PUT /admin/settings/:key` | seuils et poids, avec audit |
| `GET /admin/audit` | journal des actions de l'équipe |

Chaque appel `/admin` en écriture ajoute une ligne à `admin_audit`.
