Appendix B · API
Complete Vivra server API contract: routes, inputs, outputs, and errors. It supplements the route list on the Project analysis page. All figures in JSON responses below are examples, not live data.
General conventions#
- Base:
https://api.<vivra-domain>/v1. UTF-8 JSON throughout. - On-chain amounts returned as strings (raw units); USD amounts as numbers with 2 decimals in this specification; dates in ISO 8601 UTC.
- Cursor pagination:
?limit=50&cursor=<opaque>, response{ "items": [...], "next_cursor": "..." | null }. Maximumlimit: 100. - Voter authentication:
vivra_sessioncookie (HttpOnly, Secure, SameSite=Lax), value = session ID. No token in browser storage. - Back-office authentication: message signed by a wallet in
team_wallets(SIWE, EIP-4361), then a 12 hvivra_admincookie. - CORS: dapp and back-office domains only.
Error format#
{ "error": { "code": "vote_closed", "message": "Voting for cycle 4 closed at 2026-11-12T18:00:00Z." } }| HTTP | code | When |
|---|---|---|
| 400 | invalid_input | missing or malformed field (details in message) |
| 401 | unauthenticated | no valid session |
| 403 | forbidden | team account, blocked account, wallet not allowlisted |
| 403 | account_too_new | account younger than min_account_age_hours |
| 404 | not_found | unknown resource |
| 409 | vote_closed / vote_not_open | outside voting window |
| 422 | disposable_email | disposable email domain |
| 422 | invalid_code | incorrect or expired code |
| 429 | rate_limited | too many requests; Retry-After header |
| 500 | internal | server error with request_id |
Rate limits#
| Route | Limit |
|---|---|
POST /auth/email | 3 per email per hour, 10 per IP per hour |
POST /auth/verify | 5 attempts per code, then the code is invalidated |
POST /votes, POST /supports | 20 per session per minute |
| Public read routes | 120 per IP per minute |
Public routes (no session required)#
GET /stats#
{ "takeovers": 4, "revived": 2, "verdicts": 3,
"vivra_burned": "1250000000000000000000000", "cto_fees_received_usd": 4210.55 }GET /tokens/:address#
Only for a published token (candidate in a published cycle or a token taken over); otherwise 404. Returns the token header, latest snapshot, contract checks, socials, latest recovery score and breakdown, and 90 days of daily history (history: [{ "date", "price_usd", "volume_usd", "holders" }]).
GET /cycles/current#
{ "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 is null without a session or vote.
GET /candidates/:id#
The complete dossier: all blocks from Candidate dossier contents, plus recovery_score (score frozen at publication and its breakdown) and recovery_score_live.
GET /cycles and GET /cycles/:id#
Cycle history with the winner and final results.
GET /operations/:id#
{ "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.<vivra-domain>", "x": "https://x.com/…", "telegram": "https://t.me/…" },
"cto_request": { "status": "submitted" },
"verdict": null }GET /operations/:id/log#
Cursor pagination. Each row: { "id", "at", "agent", "kind", "summary", "cost_usd" }. Never payload.
GET /ledger#
Parameters: type, from, to, limit, cursor. Each row: type, token, raw and USD amounts, source and destination wallets, hash, date.
GET /wallets#
Public addresses of all wallets in wallets, with current balances.
Sign-in#
POST /auth/email#
Input: { "email": "[email protected]", "captcha_token": "…" }. Normalizes the email (lowercase, no surrounding spaces), rejects disposable domains, creates or retrieves the user, and emails a six-digit code plus magic link (/auth/magic?token=…). Always responds 204, whether the email exists or not.
POST /auth/verify#
Input: { "email": "[email protected]", "code": "482913" } or { "magic_token": "…" }. Creates a 30-day session and sets the cookie. Response: { "user": { "id": 812, "created_at": "…", "can_vote_at": "…" } }.
POST /auth/logout#
Revokes the current session. 204.
GET /me#
{ "id", "created_at", "can_vote_at", "votes": [{ "cycle_id", "candidate_id" }], "supports": [candidate_id] }.
Voting and support (session required)#
POST /votes#
Input: { "candidate_id": 17 }. Creates or replaces the user’s vote for that candidate’s cycle. Errors: vote_not_open, vote_closed, account_too_new, forbidden. Response: the updated GET /cycles/current object.
POST /supports and DELETE /supports/:candidate_id#
Input: { "candidate_id": 17 }. Idempotent. Response: { "supports": 413 }.
Real time (WebSocket)#
Connect to wss://api.<vivra-domain>/v1/ws. The client sends { "subscribe": "cycle:4" } or { "subscribe": "operation:4" }. Incoming messages:
type | Channel | Content |
|---|---|---|
vote.tally | cycle:* | { candidates: [{ id, votes, share_pct }], voters }, at most 1 per second |
step.updated | operation:* | { key, status, at } |
log.appended | operation:* | one public log row |
slice.executed | operation:* | { index, amount_in_wei, amount_burned, tx_hash, at } |
trade | operation:* | { price_usd, amount_usd, side, at } for the live chart |
verdict | operation:* | { verdict, at } |
Ping every 25 s; automatic client reconnection resumes from the last received id ({ "subscribe": "operation:4", "since": 1532 }).
Back-office (/admin prefix, admin session required)#
| Method and route | Purpose |
|---|---|
POST /admin/auth/nonce, POST /admin/auth/verify | team-wallet SIWE sign-in |
POST /admin/projects | submit an address and optional links; start Scanner analysis |
GET /admin/projects?status=analyzed | analyzed projects with target, safety, and recovery score |
POST /admin/projects/:address/review | { "decision": "validate" | "reject" | "rerun", "reason": "…" } |
POST /admin/cycles | create a cycle (number, times, minimum account age) |
PUT /admin/cycles/:id/candidates | set the 5 candidates and their copy; rejected after publication |
POST /admin/candidates/:id/prereport | start Scanner pre-report |
POST /admin/cycles/:id/publish | publish all 5 dossiers together; freeze scores |
GET /admin/validations?status=pending | approval queue |
POST /admin/validations/:id | { "decision": "approve" | "reject", "reason": "…" } |
POST /admin/operations/:id/pause, /resume, /stop | operation controls and emergency stop |
POST /admin/buyback/stop | emergency stop for all slices |
PUT /admin/cto-requests/:id | update Pons application status |
GET /admin/settings, PUT /admin/settings/:key | thresholds and weights, with audit |
GET /admin/audit | team action audit log |
Every write to /admin adds a row to admin_audit.