vivraDocumentationV1How voting works

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 }. Maximum limit: 100.
  • Voter authentication: vivra_session cookie (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 h vivra_admin cookie.
  • CORS: dapp and back-office domains only.

Error format#

json
{ "error": { "code": "vote_closed", "message": "Voting for cycle 4 closed at 2026-11-12T18:00:00Z." } }
HTTPcodeWhen
400invalid_inputmissing or malformed field (details in message)
401unauthenticatedno valid session
403forbiddenteam account, blocked account, wallet not allowlisted
403account_too_newaccount younger than min_account_age_hours
404not_foundunknown resource
409vote_closed / vote_not_openoutside voting window
422disposable_emaildisposable email domain
422invalid_codeincorrect or expired code
429rate_limitedtoo many requests; Retry-After header
500internalserver error with request_id

Rate limits#

RouteLimit
POST /auth/email3 per email per hour, 10 per IP per hour
POST /auth/verify5 attempts per code, then the code is invalidated
POST /votes, POST /supports20 per session per minute
Public read routes120 per IP per minute

Public routes (no session required)#

GET /stats#

json
{ "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#

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 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#

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.<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:

typeChannelContent
vote.tallycycle:*{ candidates: [{ id, votes, share_pct }], voters }, at most 1 per second
step.updatedoperation:*{ key, status, at }
log.appendedoperation:*one public log row
slice.executedoperation:*{ index, amount_in_wei, amount_burned, tx_hash, at }
tradeoperation:*{ price_usd, amount_usd, side, at } for the live chart
verdictoperation:*{ 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 routePurpose
POST /admin/auth/nonce, POST /admin/auth/verifyteam-wallet SIWE sign-in
POST /admin/projectssubmit an address and optional links; start Scanner analysis
GET /admin/projects?status=analyzedanalyzed projects with target, safety, and recovery score
POST /admin/projects/:address/review{ "decision": "validate" | "reject" | "rerun", "reason": "…" }
POST /admin/cyclescreate a cycle (number, times, minimum account age)
PUT /admin/cycles/:id/candidatesset the 5 candidates and their copy; rejected after publication
POST /admin/candidates/:id/prereportstart Scanner pre-report
POST /admin/cycles/:id/publishpublish all 5 dossiers together; freeze scores
GET /admin/validations?status=pendingapproval queue
POST /admin/validations/:id{ "decision": "approve" | "reject", "reason": "…" }
POST /admin/operations/:id/pause, /resume, /stopoperation controls and emergency stop
POST /admin/buyback/stopemergency stop for all slices
PUT /admin/cto-requests/:idupdate Pons application status
GET /admin/settings, PUT /admin/settings/:keythresholds and weights, with audit
GET /admin/auditteam action audit log

Every write to /admin adds a row to admin_audit.

↑ ↓ to navigate · Enter to openVIVRA DOCS