# Kertivo Index data schema (schemaVersion 2)

Canonical page: https://kertivo.com/trust-index/schema (JSON: https://kertivo.com/trust-index/schema.json). Generated from trust-index/v3/index-schema.json by schema-doc.mjs.

## Files
- `latest.json`: GET /trust-index/latest.json. One object per run: summary fields + results[] (one row per endpoint, raw row minus responseBody, settle and schemaVersion).
- `raw.jsonl`: GET /trust-index/raw.jsonl. One JSON object per line, one line per paid call: every results[] field plus responseBody (<=20,000 chars), settle and schemaVersion.
- `runs/<n>.jsonl`: GET /trust-index/runs/<n>.jsonl. Frozen, append-only normalized history rows of published run n (sha256 in /trust-index/runs). Never rewritten: runs 1-3 stay byte-identical. History row fields are listed under historyRow.

## Versioning
- Version 1 = Index #1-#3 files (no schemaVersion field). Treat a missing schemaVersion as 1.
- Version 2 (Index #4 on) only ADDS fields. No v1 field was renamed, removed or changed meaning, and the letter grades and verdict values are the same set.
- A future change that renames or removes a field, or changes a value's meaning, bumps schemaVersion to 3 and is announced in this document first.
- Unknown fields must be ignored by consumers; new fields can appear without a version bump only if they are optional and additive.

## latest.json summary fields

| Field | Since | Type | Meaning |
|---|---|---|---|
| `generatedAt` | v1 | string (ISO 8601 UTC) | When the run was built. |
| `run` | v1 | string | Run title, e.g. "Kertivo Index #4" (#1-#2 say "x402 Trust Index #N"). |
| `rubric` | v1 | string | Grading rubric version ("v3"). Independent of schemaVersion. |
| `payer` | v1 | string (0x address) | Wallet that paid every call. |
| `totalSpentUSDC` | v1 | number | USDC that left the payer wallet (sum of balance deltas). |
| `settledPayments` | v1 | integer | Rows with a verified on-chain settlement. |
| `caps` | v1 | object {globalUSDC, perCallUSDC} | Spend caps for the run. |
| `gradeCounts` | v1 | object grade -> count | Rows per grade. |
| `verdictCounts` | v1 | object verdict -> count | Rows per verdict. |
| `gradeChanges` | v1 | array {idx, endpoint, change, verdict} | Rows whose grade differs from the previous run. |
| `payToChanges` | v1 | array | Rows whose live 402 payTo differs from the previous run (alert=false for rotating sellers). |
| `needsManualReview` | v1 | array {endpoint, reason} | Schema calls a human must read before publishing (empty in a published run unless acknowledged). |
| `previousRun` | v1 | string\|null | File the run was compared against. |
| `results` | v1 | array of row | One row per endpoint; fields below. |
| `schemaVersion` | v2 | integer | Data schema version of this file (2). |
| `schemaDoc` | v2 | string (URL) | This document. |
| `totalSettledUSDC` | v2 | number | Sum of row settledUSDC: USDC we saw move on-chain to sellers. Normally equals totalSpentUSDC; a gap means money left the wallet without a verified settlement (see charged_settlement_unverified rows). |
| `subGradeCounts` | v2 | object subGrade -> count | Facilitator-refused rows per sub-grade (facilitator_account, seller_requirements, facilitator_other). |
| `thinPaidResponses` | v2 | array {idx, endpoint, grade, flags[], detail} | Settled 2xx calls whose data was thin or wrong (see row.thinData). The case a charge-on-success gateway still charges for. |
| `delivery` | v2 | object {paidRows, A, B, F, aRate, usableRate, basis} | Headline delivery rate over paid rows only (rows where money moved: grades A, B, F). aRate = A / paidRows, usableRate = (A + B) / paidRows, 4 decimals, null with no paid rows. Dead listings (freshness never-live / went-dark) and ungraded rows are not in the denominator. Index #3 under this rule: 78 A of 87 paid = 0.8966. |
| `freshnessCounts` | v2 | object {live, went-dark, never-live, ungraded, retired} | Rows per row.freshness; retired = rows dropped by the prune plan this run (summary.retired). Listing freshness is reported separately from delivery. |
| `movers` | v2 | object {up[], down[], wentDark[], new[], retired[], unchanged, counts}\|null | Grade movement vs the previous run. Order A > B > C > F. wentDark = A/B last run, C now. retired = in the previous run, not in this one, with reason (dead_twice from the prune plan, or not_in_this_run). |
| `retired` | v2 | array {endpoint, reason, evidence[]} | Rows deliberately dropped from this run's sample by the prune plan (graded C this run and bad in a second observation), so they don't silently vanish. |

## Row fields (latest.json results[] and raw.jsonl)

| Field | Since | Type | Meaning |
|---|---|---|---|
| `idx` | v1 | integer | Row number in this run (selection order). |
| `cohort` | v1 | string | Selection batch the endpoint came in with (pilot, new, added-#3, added-#4, self, ...). |
| `endpoint` | v1 | string (URL) | Resource URL as listed (join key across runs). |
| `service` | v1 | string | Seller/service name. |
| `category` | v1 | string | Rough category. |
| `timestamp` | v1 | string (ISO) | When the paid attempt started (or the probe, if not paid). |
| `request` | v1 | object {url, method, body} | Exactly what we sent. |
| `replay` | v1 | object | Replayable request: method, url, headers, body, curlUnpaid (reproduces the unpaid 402), howToPay. |
| `verdict` | v1 | enum | What happened, separate from the grade. v2 note: a 402 answering our payment with a facilitator error code relayed only in the 402 body's error field (e.g. invalid_payment_requirements) now also counts as paid_attempt_failed_facilitator_refused (before: paid_attempt_rejected_402). Same C grade either way; no Index #1-#3 row is affected. Values: `paid_ok`, `paid_no_delivery`, `charged_settlement_unverified`, `paid_attempt_failed_facilitator_refused`, `paid_attempt_rejected_402`, `paid_attempt_network_error`, `paid_attempt_failed_other`, `unpaid_probe_not_402`, `unparseable_402`, `no_base_usdc_option`, `skipped_by_cap_or_policy`, `not_attempted`, `probe_error`. |
| `unpaid402` | v1 | object {status, ms, x402Version, source, valid, amount, payTo, maxTimeoutSeconds} | The unpaid probe and the Base USDC option we paid against. |
| `priceUSDC` | v1 | number\|null | Listed price from the live 402. |
| `paidTxHash` | v1 | string\|null | Settlement tx (Base). |
| `onchain` | v1 | object\|null | Receipt check: status, block, transferFound, value (atomic USDC), txHash. |
| `settled` | v1 | boolean | A USDC Transfer from payer to payTo was found in a successful tx. |
| `balanceDeltaAtomic` | v1 | string\|null | Payer balance change around the call (atomic USDC, 6 decimals). |
| `httpStatus` | v1 | integer\|null | HTTP status of the paid response (200 after an async poll that delivered). |
| `latencyMs` | v1 | integer\|null | Paid call latency (end to end for async). |
| `latencyThresholdMs` | v1 | integer | Slow threshold: the 402's maxTimeoutSeconds, else 5 s. |
| `latencyThresholdSource` | v1 | string | Where the threshold came from. |
| `within5s` | v1 | boolean | latencyMs <= 5000. |
| `schemaCheck` | v1 | enum y\|partial\|n | Advertised output example paths present: y >= 80%, partial > 0, n none / no 200. |
| `schemaNote` | v1 | string | Human-readable schema result. |
| `schemaSource` | v1 | enum auto\|manual\|auto-manual-review-needed\|none | Who judged the schema. |
| `schemaAutoCoverage` | v1 | number\|null | Share of example paths present (0-1). |
| `bodyBytes` | v1 | integer\|null | Paid response size. |
| `bodyFile` | v1 | string | Archive path of the full body. |
| `responseBodyTruncatedInRow` | v1 | boolean | raw.jsonl responseBody was cut at 20,000 chars. |
| `grade` | v1 | enum A\|B\|F\|C-went-dark\|C-never-live\|ungraded | Rubric v3 grade (unchanged since v2 thresholds). |
| `everDelivered` | v1 | boolean | Delivered now or in any earlier run (decides C-went-dark vs C-never-live). |
| `prevGrade` | v1 | string\|null | Grade in the previous run. |
| `gradeChange` | v1 | string\|null | "X -> Y" when the grade changed. |
| `pilotGrade` | v1 | string\|null | Grade in the Index #1 pilot. |
| `priorAttempts` | v1 | any\|null | Earlier attempts recorded in the run state. |
| `note` | v1 | string\|null | Runner notes. |
| `async` | v1 | object\|null | Async poll record (202 queue -> statusUrl -> completed body). |
| `paidStatusImmediate` | v1 | integer\|null | HTTP status before an async poll. |
| `guards` | v1 | object\|null | Grader guards applied (e.g. sig0xFixed). |
| `payToChange` (optional) | v1 | object | Present when the live payTo differs from the previous run or selection. |
| `payToRotates` (optional) | v1 | true | Seller uses per-request deposit addresses. |
| `payToRotatesEvidence` (optional) | v1 | string | Evidence for payToRotates. |
| `rubric` | v1 | string | Rubric version for this row. |
| `settledUSDC` | v2 | number | USDC that actually moved on-chain for this call: the Transfer value in the settle tx (onchain.value / 1e6). 0 when nothing verifiably moved. Check it on Basescan with paidTxHash. |
| `refusal` | v2 | object {side, code, label, fixIn}\|null | Only on paid_attempt_failed_facilitator_refused rows: the facilitator error string classified. side = facilitator_account (billing/account side, e.g. CDP payment-method-required; fix in the facilitator portal), seller_requirements (the requirements the seller sent were rejected, e.g. invalid_payment_requirements; fix in the seller's code), or facilitator_other (not classifiable). code = the matched error code. |
| `subGrade` | v2 | enum facilitator_account\|seller_requirements\|facilitator_other\|null | refusal.side, flat for filtering. The letter grade stays C; the sub-grade says who has to fix it. |
| `thinData` | v2 | object {flags[], problems, detail}\|null | Only on settled 2xx rows (null otherwise). flags: empty_body, non_json_body, missing_advertised_fields, error_in_2xx (error/success:false/ok:false/status error inside a 2xx), empty_result (an array the seller's example shows filled came back empty), slow (over latencyThresholdMs). detail.weakAdvertisedSchema notes an example with at most one field (a match proves little). |
| `freshness` | v2 | enum live\|went-dark\|never-live\|ungraded | Listing freshness, separate from the delivery grade. live = a payment settled this run (grade A, B or F); went-dark = grade C-went-dark (delivered in an earlier run, nothing this run); never-live = grade C-never-live (never delivered in any run we saw; may still answer a valid unpaid 402, see unpaid402.valid); ungraded = not attempted. Retired listings are not rows; they are in summary.retired. |
| `lastSeenLive` | v2 | string (ISO)\|null | Last time we saw this listing live: this row's timestamp when live, else the previous run's lastSeenLive, else the previous run's timestamp if it was paid then (A/B/F). null = unknown or never (e.g. delivered only in the pre-#2 pilot, which has no per-row time). |
| `lastProbeOk` | v2 | string (ISO)\|null | Time of the most recent VALID unpaid 402 we saw for this listing (x402 v2 with a Base USDC exact option carrying amount, payTo and maxTimeoutSeconds; same test as unpaid402.valid), null if never. This run: when the unpaid probe answered (runner probeAt; for states recorded before probeAt existed, the row start time, which the probe follows within a second). If this run's probe failed: the previous run's lastProbeOk, else the previous run's row timestamp when its unpaid402 was valid. Shown first in the Freshness column with its age, so buyers can discount stale rows. Independent of lastSeenLive (last settled + delivered). |
| `responseBodySha256` | v2 | string (hex)\|null | Index #4 on: sha256 of the exact bytes of the archived paid response body (bodyFile; for async deliveries, the completed body we graded). Lets anyone holding the body recheck a disputed grade without re-probing or re-paying. null when no body was saved. |
| `schemaSnapshotSha256` | v2 | string (hex)\|null | Index #4 on: sha256 of the canonical JSON (keys sorted at every depth, no whitespace) of the advertised schema we graded against: {liveOutputSchema, output, schema} as stored in raw.jsonl schemaSnapshot. Same advertised schema = same hash, so a later listing change is detectable. null when the seller advertised nothing. |

## raw.jsonl only

| Field | Since | Type | Meaning |
|---|---|---|---|
| `responseBody` | v1 | string | Paid response body, up to 20,000 chars. |
| `settle` | v1 | object\|null | Decoded PAYMENT-RESPONSE settle header (facilitator errorReason lives here). |
| `schemaVersion` | v2 | integer | Data schema version of this line (2). Missing = 1. |
| `schemaSnapshot` | v2 | object\|null | Index #4 on: the advertised schema we graded against: output (listing output block: example, asyncPoll), schema (listing schema), liveOutputSchema (outputSchema from the live 402, if sent), plus canonicalBytes, judgedPaths (sorted paths the schema check used) and judgedPathsSource. Hash in row.schemaSnapshotSha256. |

## Frozen history rows (runs/<n>.jsonl)

| Field | Since | Type | Meaning |
|---|---|---|---|
| `run` | v1 | integer | Run number. |
| `runAt` | v1 | string (ISO) | When the row was paid (or the run time). |
| `endpoint` | v1 | string (URL) | Resource URL. |
| `service` | v1 | string\|null | Seller/service name. |
| `category` | v1 | string\|null | Category. |
| `grade` | v1 | string\|null | Grade in that run. |
| `priceUSDC` | v1 | number\|null | Listed price. |
| `payTo` | v1 | string\|null | payTo from the 402. |
| `paidTxHash` | v1 | string\|null | Settlement tx. |
| `settled` | v1 | boolean | Verified on-chain settlement. |
| `httpStatusAfterPay` | v1 | integer\|null | HTTP status after paying. |
| `latencyMs` | v1 | integer\|null | Paid latency. |
| `schemaCheck` | v1 | string\|null | y / partial / n. |
| `settledUSDC` (optional) | v2 | number | Run #4 on: USDC that moved on-chain for this call (0 = none). Absent in runs 1-3 (frozen). |
| `subGrade` (optional) | v2 | string | Run #4 on, only on facilitator-refused rows: facilitator_account \| seller_requirements \| facilitator_other. |
| `freshness` (optional) | v2 | string | Run #4 on: live \| went-dark \| never-live \| ungraded. Absent in runs 1-3 (frozen). |
| `lastSeenLive` (optional) | v2 | string (ISO)\|null | Run #4 on: see row.lastSeenLive. Absent in runs 1-3 (frozen). |
| `lastProbeOk` (optional) | v2 | string (ISO)\|null | Run #4 on: see row.lastProbeOk. Absent in runs 1-3 (frozen). |
| `responseBodySha256` (optional) | v2 | string (hex)\|null | Run #4 on: see row.responseBodySha256. Absent in runs 1-3 (frozen). |
| `schemaSnapshotSha256` (optional) | v2 | string (hex)\|null | Run #4 on: see row.schemaSnapshotSha256. Absent in runs 1-3 (frozen). |
