{
  "version": "0.2",
  "grades": {
    "A": "≥90",
    "B": "≥80",
    "C": "≥70",
    "D": "≥55",
    "F": "<55"
  },
  "verdict_rules": {
    "unknown": "not indexed, coverage < 0.5, or blocked — and also what a max_age miss degrades to. Those two kinds of `unknown` return different things and are billed differently; see `billing`",
    "avoid": "any critical flag",
    "caution": "score < 70, stale, coverage < 0.8, or tier 0 only",
    "ok": "score ≥ 70, no flags, fresh, coverage ≥ 0.8"
  },
  "billing": {
    "rule": "you are charged for scored data delivered, never for a verdict we could not form",
    "free": "`unknown` because we have no answer: NOT_INDEXED, NEVER_SWEPT, COLLECTION_BLOCKED, APP_URL_UNRESOLVED, APP_UNREACHABLE, COVERAGE_INSUFFICIENT, NO_DIMENSIONS_EVALUATED. The response carries the verdict and its reason codes, and OMITS score/coverage/tier/flags/dimensions/freshness entirely — absent, not null.",
    "paid": "every response carrying a scored observation, including `unknown` with reason STALE_FOR_CALLER, where we do have data and it merely fails the max_age you declared. You get the observation with its checked_at and the warning attached; the warning is not a discount.",
    "both": "max_age against an app we cannot grade resolves to the free branch — the test is whether scored data is delivered, and there is none to deliver."
  },
  "asymmetry": "tier-0 static checks can condemn a dApp but never clear one — tier 0 alone is never `ok` (§2.3)",
  "reason_codes": {
    "NOT_INDEXED": "no binding for this address or url — never read as safe",
    "NEVER_SWEPT": "bound to an app that has never been observed",
    "AMBIGUOUS_BINDING": "address maps to several apps; inspect resolved.candidates rather than assuming",
    "COLLECTION_BLOCKED": "robots.txt or a rate limit stopped collection — an absence of data, not a finding about the app",
    "APP_URL_UNRESOLVED": "no distinct app URL could be resolved; not scored rather than scoring the marketing page",
    "APP_UNREACHABLE": "the app did not respond at the last check",
    "COVERAGE_INSUFFICIENT": "under half the rubric ran — too little to grade",
    "NO_DIMENSIONS_EVALUATED": "nothing scoreable ran",
    "CRITICAL_FLAG": "a critical failure from a check that did run; see `flag`",
    "CHECK_FAILED": "an individual check failed; see `check` for its id and `detail` for what was observed",
    "SCORE_BELOW_THRESHOLD": "score under 70",
    "COVERAGE_BELOW_TARGET": "coverage under 0.8 — the rubric ran, but not enough of it to clear the app",
    "TIER_0_ONLY": "only tier-0 static checks ran; §2.3 — they can condemn an app but never clear one",
    "STALE": "older than our own freshness SLA (26h)",
    "STALE_FOR_CALLER": "older than the max_age the caller asked for; the observation is still included, with its checked_at, so you can decide — and because the data is delivered, this `unknown` is a normal paid response, unlike the ones above"
  },
  "freshness": {
    "cadence_seconds": 86400,
    "stale_after_seconds": 93600,
    "max_age": "optional /v1/score query param, in seconds. If the latest observation is older, the verdict is `unknown` with reason STALE_FOR_CALLER and the stale observation still travels in the response, with its checked_at, so you can decide for yourself. Because the data is delivered, this is a normal paid response — it is a warning on the answer, not a cheaper answer. Omit the param for the default behavior."
  },
  "tier0_checks": {
    "availability.reachable": 25,
    "trust.tls": 10,
    "trust.headers": 15,
    "trust.mixed_content": 5,
    "trust.audit_disclosure": 15,
    "trust.link_integrity": 15
  },
  "pricing": {
    "score.cached": "$0.01–0.05",
    "score.unknown": "free — no verdict could be formed, and nothing scored is returned",
    "score.stale_for_caller": "$0.01–0.05 — verdict `unknown`, but the stale observation is delivered",
    "recheck": "$0.50–2.00",
    "report": "$25–100",
    "history": "subscription",
    "monitoring": "$100–500/mo"
  }
}