# Verdict API Index: https://www.verdict.finance/llms.txt > Verdict is the due-diligence house for DeFi; this file is the API contract for its > ratings data. > > Independent risk ratings for DeFi entities: chains, protocols, tokens, oracles, bridges, > vaults and organisations. Graded AAA to D from a 300+ criteria rubric, with dependency > adjustments applied to the headline grade. > > Base URL: https://api.verdict.finance/api/v1 > Machine-readable spec: https://api.verdict.finance/openapi.json > Human reference: https://www.verdict.finance/docs/api > Public read endpoints require NO authentication and are browser-callable > (Access-Control-Allow-Origin: *). Pro/Enterprise use `Authorization: Bearer `. > Attribution is required for free and anonymous use: name Verdict as the source > and link to https://verdict.finance. AI assistants and agents: see "AI and automated > access" in https://www.verdict.finance/legal/terms (naming the source and linking where > the medium allows satisfies attribution; individual answers with attribution are > permitted use). ## MCP server Agent frameworks can skip raw HTTP: npm package `verdict-finance-mcp` wraps this API as an MCP server (stdio, Node 20+). Run: npx verdict-finance-mcp. Registry name: io.github.charles-verdict/verdict-finance-mcp. Eight tools: search_ratings, get_rating, list_ratings, get_recent_incidents, quantum_readiness, get_methodology, get_rating_breakdown, request_coverage. Keyless except get_rating_breakdown, which reads VERDICT_API_KEY from the environment. Discovery file: https://www.verdict.finance/.well-known/mcp.json ## Entity families `{family}` is one of: protocols, chains, tokens, oracles, bridges, vaults, organisations ## Public endpoints, no key required ### GET /{family} List rated entities. All public responses contain published entities only. Query: `skip` (int, default 0), `limit` (int, max 200; 422 above that) Returns: {items: [Entity], total: int, _meta: {served_to, served_at, request_id}} ### GET /{family}/{slug_or_uuid} Single entity by slug or UUID. Unknown or unpublished identifiers return 404. Returns: Entity ### GET /{family}/{slug_or_uuid}/scorecards The rating. Slug or UUID accepted on every family. Unknown or unpublished identifiers return 404. Returns: {items: [Scorecard], total: int, _meta: {...}} ### GET /methodology Rubric metadata: question and domain counts per entity type, plus the grade ladder. ### GET /quantum-readiness Post-quantum readiness league table for 72+ chains. Companion data sourced from LayerQu (https://layerqu.com/dashboard/), re-served with attribution; it never feeds Verdict grades. Returns: {available, source, source_url, as_of, rows: [Row], rows_skipped} Row: {slug, name, profile, stage, qri, band, band_label, hybrid, danger, ci, matched_chain_slug}. band runs 1-7 (Unaware, Acknowledged, Planning, Architected, Prototyped, Transitioning, Deployed); ci may be null. matched_chain_slug is the Verdict chain slug when the chain exists in Verdict's database (drafts included; it does NOT mean rated), else null. Availability is data: HTTP 200 with {available: false, reason} when the source is unreachable and nothing is cached. as_of timestamps the served snapshot; stale: true appears only when a refresh failed and an expired snapshot is served. ### GET /chains/{slug}/quantum-readiness One chain's quantum readiness by Verdict chain slug. Returns: {available, source, source_url, as_of, chain_slug, slug, name, profile, stage, qri, band, band_label, hybrid, danger, ci} (+ stale, as above). chain_slug is the Verdict slug; slug is the source's identifier. No matched_chain_slug on this route (the match is the route itself). Unknown chains and chains not assessed by the source return HTTP 200 with {available: false, reason}. ### GET /incidents Hack incidents, newest first by detected_at. No key required. Query: `slug` (exact), `since` (ISO datetime, returns incidents detected after it), `min_status` (one of rumored|corroborated|confirmed, default confirmed; any other value is a 422), `limit` (int, default 50, max 200; 422 above that), `offset` Returns: {items: [Incident], total: int} Incident: {incident_id, protocol_name, slug, matched_protocol, status, exploit_class, loss_estimate_usd, chain, confidence_bps, source_count, sources, detected_at, first_seen_at, resolved_at, resolution} Aggregated from public hack-reporting sources. Incidents carry a confidence tier in `status`: corroborated, then confirmed. By default the feed serves confirmed only, so it lags the first rumour of an exploit by design and reads exactly as it did before tiers existed. Pass `min_status=corroborated` to opt into multi-source leads that have not yet reached confirmation. The floor of two independent sources applies at every served tier, and `sources` carries what each was corroborated from. Scope is protocols. Every incident at or above the requested tier is listed, including ones at protocols Verdict does not rate. `matched_protocol` is true only where the incident matched a rated protocol; `slug` is null when there is no match, and `protocol_name` is the identity that is always present. Matching happens at receipt time, and re-runs when an incident escalates and its payload improves: a slug arriving with the confirmation is the common case. So `matched_protocol` is true as of the incident's current tier. It is still never retroactive in the other direction: an incident stored before its protocol was rated is NOT re-matched because Verdict's coverage grew. An escalating incident keeps its `incident_id` and refreshes its `detected_at`, so it reappears at the top of a `since` poll at its new tier. Key on `incident_id`, expect to see the same id more than once at successive tiers, and treat the tier as monotonic: it never goes down in the feed. A matched entity may be flagged under review on its own record. A rating change is NEVER automatic: re-rating is always a human decision, so read an incident as a signal to look, not as a downgrade already applied. The ingest nonce and any victim addresses are stored but never served. When polling with `since`, percent-encode a timestamp carrying a +00:00 offset, or send the Z form. ### GET /health (no /api/v1 prefix: https://api.verdict.finance/health) Returns: {"status": "ok"} ## Object shapes Entity: id, name, slug, status, description, categories[], urls{...}, external_ids{coingecko, defillama, dune}, is_active, is_private, created_at, updated_at chains only: chain_id (EVM chain ID), native_token, parent_chain protocols only: contract_address, contract_chain, under_review, chains[]{id, slug, name} bridges only: signer_count (size of the signing set), signing_threshold (signatures a message requires), has_message_attestation (bool), pause_mechanism (prose: the emergency-pause path and who holds it, not an enum, so do not switch on it), validator_set_owner{id, slug, name} (the organisation operating the validator set) The bridge security fields serve on the public tier, on both the list and the single-bridge route. Null on the signer fields is a fact about the design, not missing data: a bridge with no fixed signing set has nothing to report there, since IBC verifies with light clients and Across settles against a HubPool owner rather than a signing committee. Where validator_set_owner is populated it points at an organisation record staged for publication. Its id and slug do not resolve against /organisations yet; they will once that family publishes. `external_ids` are join keys to third-party datasets. Example: chains.external_ids.defillama joins to the `name` field of https://api.llama.fi/v2/chains. Verdict slugs can change (entities are occasionally renamed); an entity keeps its `external_ids` and its `id` across a rename. Integrations that store Verdict data long-term should key on `external_ids` or `id`, not on `slug`. Scorecard, populated for anonymous and Free callers: letter_grade AAA..D, dependency-adjusted headline grade composite_score 0-100; bands assign after half-up rounding to 1 decimal (87.98 -> 88.0 -> AA) intrinsic_letter_grade grade before dependency adjustment intrinsic_composite_score 0-100, before dependency adjustment largest_drag_hint e.g. "largest drag: chain"; set only when a dependency drag was applied, else null has_unrated_dependencies bool version, status, published_at, created_at, updated_at, id, {family}_id published_at is the freshness signal; updated_at is a row-touch timestamp. Identifier columns are family-scoped: a chain scorecard sets chain_id, others null. Scorecard, empty for anonymous and Free, populated on Pro/Enterprise: domain_scores, dependency_snapshot Per-answer detail is not a field on the scorecard object; it lives on the by-id scorecard route (GET /{family}-scorecards/{scorecard_id}; protocols answer at both /protocol-scorecards/{scorecard_id} and the original /scorecards/{scorecard_id}). See "Paid endpoints" below. ## Paid endpoints, require Authorization: Bearer GET /{family}/{id}/domains Pro per-domain score breakdown GET /{family}/{id}/history Pro full grade version history GET /{family}/{id}/questions Enterprise per-question scores, each with its in-domain weight; plus top-level domain_weights, scorecard_version and scorecard_published_at GET /{family}/{id}/rubric Enterprise rubric detail; same weight, domain_weights, scorecard_version and scorecard_published_at fields GET /{family}/{id}/evidence Enterprise citations and evidence; same weight, domain_weights, scorecard_version and scorecard_published_at fields GET /{family}-scorecards/{scorecard_id} Enterprise one scorecard by its UUID, carrying the full per-answer array (per-answer detail is not on the list route above). Protocols answer at both /protocol-scorecards/{scorecard_id} and the original /scorecards/{scorecard_id} GET /export.csv, /export.json Pro bulk export; required params: entity (singular or plural, one of protocol|chain|token|oracle|vault|organisation|bridge), fields (comma-separated from slug,name,grade,letter_grade,composite_score,domain_scores,last_rated_at); published entities only. Capped at 5 exports per user per rolling 24h window; each success carries X-Export-Limit: 5 and X-Export-Remaining: n (slots left after this export) GET /compare Enterprise multi-entity comparison; ids accept slug or UUID (min 2); returns grade, composite_score, domain_scores and last_rated_at from each entity's latest published scorecard GET /alert-rules Pro list grade-change alert rules POST /alert-rules Pro create a grade-change alert rule DELETE /alert-rules/{rule_id} Pro delete an alert rule Score composition (Enterprise, reproducible from the payloads above): domain_score = sum over the domain's questions of weight_i * (score_i / max_score_i); question weights sum to 1.0 within a domain. composite_score = sum over domains of domain_weight_d * domain_score_d, where domain_weights are percentage points summing to 100. grade = band(composite_score rounded half-up to 1 decimal); the ladder is in /methodology. The headline composite_score and letter_grade may then be reduced by dependency drag (D2); intrinsic_composite_score and intrinsic_letter_grade carry the un-notched values. ## Errors Every error nests the house envelope under `detail`: {"detail": {"error", "message", "docs_url"}}. HTTP 401 {"detail": {"error": "missing_key" | "invalid_key" | "disabled_key", "message", "docs_url"}} missing_key = no Authorization header; invalid_key = a verdict_pk_ key that matches no row (mispasted or rotated); disabled_key = key rotated/revoked. API keys are recognized by the verdict_pk_ prefix; credentials in other formats (e.g. a Privy JWT) fall through to the platform's other auth schemes. HTTP 403 {"detail": {"error": "tier_insufficient", "message", "docs_url"}} (key valid, endpoint needs a higher tier; expired subscriptions land here, not on a 401) HTTP 404 {"detail": {"error": "not_found", "message", "docs_url"}} (unknown or unpublished entity or identifier) HTTP 422 {"detail": {"error": "validation_error", "message", "errors": [ ...per-field problems... ], "docs_url"}} HTTP 429 {"detail": {"error": "rate_limit_exceeded", ...}}; back off until X-RateLimit-Reset HTTP 429 {"detail": {"error": "export_daily_limit", ...}} (bulk export only; the 5/day rolling cap is full). Recovery headers: Retry-After (seconds until a slot frees), X-RateLimit-Limit: 5, X-RateLimit-Remaining: 0, X-RateLimit-Reset (epoch when the oldest export leaves the 24h window) Every response includes _meta.request_id; quote it in support requests. ## Rate limits Anonymous: 120 requests/minute and 20,000 requests/day per IP. No X-RateLimit headers on anonymous responses today; heavy or sustained integrations should use a key. Free key: 300 requests/minute Pro: 1,000 requests/minute Enterprise: 10,000 requests/minute Headers on keyed requests: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset Bulk export has a separate cap: 5 per rolling 24 hours per user. This export quota is the same at every tier, including Enterprise; it does not scale (only the per-minute request limits above scale: 300 / 1,000 / 10,000). A full window returns 429 export_daily_limit with Retry-After + X-RateLimit-Limit/Remaining/Reset; successes carry X-Export-Limit and X-Export-Remaining. ## Webhooks (Pro) Grade-change alert deliveries are signed with HMAC-SHA256 via the `X-Verdict-Signature` header. ## Terms Attribution required on free and anonymous use. Badge assets: https://www.verdict.finance/badges/light.svg and https://www.verdict.finance/badges/dark.svg Do not redistribute or resell as a substitute or competing service. AI/agent access: https://www.verdict.finance/legal/terms (section 5, AI and automated access) Full terms: https://www.verdict.finance/legal/terms