Skip to content
Veritydocs
Documentation

Build with Verity

Everything to use Verity in your own work: the calibrated comparison REST API, the native X3P codec for Rust, Python, and R, the data catalog, and the concepts behind the numbers. For the science narrative, see the method and why Verity exists.

Overview

Verity turns a pair of 3-D surface-topography scans into a calibrated, bounded likelihood ratio — an auditable weight of evidence — with a characterized cost (Cllr) and region-level attribution. One method spans striated marks (bullet lands, toolmarks) and impressed marks (cartridge breech faces).

There are four ways in: the hosted web app at verity.codes, the REST API below, the MCP server for AI agents, and the open-source Rust/Python/R libraries. The decision always stays behind a glass-box statistical firewall — the representation only produces a score; the reportable LR is a monotone, bounded transform of it.

Get going

Quickstart

The fastest path is the hosted API. GET /health lists the calibrated mark types; POST /compare returns the report. For bullets, upload allof each bullet’s land scans — aggregating the lands is the strong path.

# Which mark types have a calibrated reference?
curl -s https://api.verity.codes/health

# Compare two bullets — upload every land scan of each.
curl -s -X POST https://api.verity.codes/compare \
  -F domain=striated \
  -F mark_a=@bulletA_land1.x3p -F mark_a=@bulletA_land2.x3p \
  -F mark_b=@bulletB_land1.x3p -F mark_b=@bulletB_land2.x3p

No .x3p files handy? The web app’s Compare workspace lets you pick two real specimens from a curated gallery — bullets, cartridge cases, or toolmarks — and runs a real, precomputed comparison, no file of your own needed.

The ideas

Core concepts

Likelihood ratio (LR).The reportable answer is not a “match” — it is how much more probable the observed similarity is if the marks share a source than if they do not. Verbal equivalents (“moderately strong support”) map directly from the LR.

Calibration & Cllr. A raw score is not evidence until it is calibrated against a namedreference of known same-source (KM) and different-source (KNM) pairs. Cllr is the cost of the calibrated LRs (lower is better; <1 is informative); the Cllr−Cllrmin gap is the calibration loss. Verity also reports an empirical cap (ELUB-inspired) — the strongest claim the reference data can support.

The calibration model. The score → LR map is a 2-parameter logistic fit (Platt scaling) by default — a monotone, bounded transform, with an optional pool-adjacent-violators (isotonic) alternative. The near-unregularized logistic on a small anchoring set gives the ELUB-style bound. Source: decision/lr.py.

Congruent Matching Regions (CMR).Verity’s domain-general similarity principle: partition a mark into regions, register each against the other mark, and count the regions that agree on one common geometry. It generalizes Song’s Congruent Matching Cells (CMC) from 2-D cells to regions of any dimension — the set of congruent regions is the attribution map.

ModalityRegionTransform groupReduces to
Striated1-D profile window1-D translationChumbley / CMS
Impressed2-D grid cell2-D translation + rotation= CMC
Fractured3-D mesh patch3-D rigid poseresearch

Full write-up: docs/congruent-matching-regions.md

HTTP

REST API

Base URL https://api.verity.codes. The API is OpenAPI-described — an interactive reference (Scalar) lives at /scalar (Swagger UI at /docs, ReDoc at /redoc).

GET/health
Service status and the list of calibrated domains.
200 OK
{ "status": "ok", "engine_version": "0.1.0", "domains": ["impressed", "striated", "toolmark"], "rate_limiter": { "tracked_ips": 5 } }
POST/detect
Suggest a mark type from one scan (multipart field scan), from striation anisotropy. The UI pre-selects it; you confirm.
curl
curl -s -X POST https://api.verity.codes/detect -F scan=@mark.x3p
# { "domain": "striated", "coherence": 0.72 }
POST/compare
The core call. Multipart form: domain (striated, impressed, or toolmark), plus repeated mark_a / mark_b file fields. For striated bullets, send every land scan of each bullet; for impressed, one breech-face scan per mark; for toolmark, one striated profile scan per mark.
POST/v1/compare/report.pdf
The same comparison, returned as a court-ready PDF: the calibrated LR with its credible interval and verbal weight, the named-scope statement, the reference and its cost, the method version, the SHA-256 provenance of every input scan, and the attribution overlay.
POST/v1/scope
Applicability-domain check for one scan — whether it falls inside the validated domain (resolution, mark type, coverage, signal). The basis for refusing an out-of-domain LR rather than returning a junk number.

Response — the ComparisonReport

200 OK (annotated)
{
  "domain": "striated",
  "likelihood_ratio": 146.0,
  "log10_lr": 2.16,
  "direction": "same source",
  "verbal": "moderately strong support for same source",
  "lr_bound_log10": 2.16,
  "log10_lr_ci_lo": 1.74, "log10_lr_ci_hi": 2.16,  // 95% credible interval (clustered bootstrap)
  "lr_ci_method": "bootstrap-clustered",
  "score": 0.153,
  "score_kind": "bullet-contrast",
  "reference": {                                  // diagnostics are the in-sample fit;
    "name": "pooled bullet-land reference (Hamby-252 & 173, …)",  // source-disjoint Cllr ≈ 0.19
    "n_km": 146, "n_knm": 1755, "auc": 0.984, "cllr": 0.193, "cllr_min": 0.168
  },
  "attribution":   [{ "x_frac": 0.0,  "w_frac": 0.167, "corr": 0.91, "…": "…" }],
  "attribution_b": [{ "x_frac": 0.008,"w_frac": 0.167, "corr": 0.91, "…": "…" }],
  "previews": { "a": "[[…]] downsampled height grid", "b": "[[…]]" },
  "scope_note": "A calibrated weight of evidence on the named reference. Not a
                 verdict: one input to an examiner's judgment, alongside case
                 context. Not a claim about the error rate of examination, which
                 remains unknown."
}
likelihood_ratioThe calibrated LR — the reportable weight of evidence (LR < 1 supports different sources).
log10_lr · lr_bound_log10log₁₀ of the LR, and the empirical cap — the strongest claim the reference supports.
verbal · directionVerbal equivalent of the LR, and which source hypothesis it favors.
score · score_kindThe raw similarity score and which scorer produced it.
referenceThe named calibration population and its diagnostics (n_km, n_knm, auc, cllr, cllr_min).
attribution · attribution_bThe matched regions on Mark A and Mark B — the explanation of the score.
previewsDownsampled height grids of the rendered surfaces, for overlaying attribution.
scope_noteAn explicit statement of what the number does and does not claim.

CORS: the API answers the web origin (configurable via VERITY_CORS_ORIGINS). Errors return a JSON detail with a 400 status for bad uploads or an uncalibrated domain.

For agents

MCP server

Verity ships a Model Context Protocol server (“verity”) so AI agents can drive the same calibrated engine as the REST API — behind the same glass-box firewall and the same scope-note guarantees.

POST/mcp
Hosted remote server (streamable HTTP) at https://api.verity.codes/mcp — scans go in as base64, no local install.

Or run it locally over stdio against any API base:

stdio
uv run --directory services/mcp verity-mcp   # env VERITY_API_URL

Six tools — compare_marks, detect_mark_type, calibrate_score, list_references, scorer_config, and service_health — carry the same calibration firewall and scope-note guarantees as the HTTP API. A Claude Desktop bundle builds from services/mcp/build_mcpb.sh.

Show your work

Reproducibility & the glass-box API

Every /v1/compare carries a recipe— the methods section as JSON: every pipeline step and its parameters, the engine version, the SHA-256 of each input scan, and the reference’s provenance — stamped with a content handle. Same inputs + scorer config + reference + engine produce the same handle, so verifying a published likelihood ratio is a hash-equality check, not an act of trust.

recipe (excerpt)
{
  "handle": "sha256:632d8a8b9943fb71…",        // the content address of this computation
  "engine_version": "0.1.0",
  "scorer_config_hash": "ea4ddd513b57…",
  "inputs": { "mark_a": ["8ab45f56…"], "mark_b": ["ed232b90…"] },   // SHA-256 of each scan
  "reference": {
    "name": "Fadul cartridge cases", "scorer_config_hash": "ea4ddd513b57…",
    // in-sample reference fit, not a validation claim — the frozen public
    // cartridge benchmark reads AUC 0.922 (fold mean; see /benchmark)
    "diagnostics": { "n_km": 10, "n_knm": 180, "auc": 0.997, "cllr_min": 0.07 }
  },
  "result": { "likelihood_ratio": 10.0, "log10_lr": 1.0, "verbal": "moderate support…" },
  "steps": [
    { "step": "decode",          "code": "verity_x3p.read_x3p" },
    { "step": "preprocess",      "code": "verity.preprocess", "params": { "lambda_s": 4e-6, "lambda_c": 250e-6 } },
    { "step": "areal-signature", "code": "verity.areal.areal_signature" },
    { "step": "compare",         "code": "verity.cmr.cmr_count" },
    { "step": "calibrate",       "code": "verity.decision.lr.ScoreLRModel", "params": { "lr_bound": "auto" } },
    { "step": "uncertainty",     "code": "verity.decision.uncertainty.lr_credible_interval" }
  ]
}

Every step is addressable. Upload a scan for a content handle, then chain the pipeline — each intermediate is fetchable and links to its inputs, a content-addressed graph from scan to score.

POST/v1/artifacts
Upload a scan → a surface handle (the graph entry point).
POST/v1/steps/{preprocess,signature,areal-signature,align,features}
Each wraps one engine step, taking its inputs as content handles and returning the output handle + provenance.
GET/v1/artifacts/{handle}
The intermediate’s record (kind + provenance); add /data for the raw .npy (ETag = handle) or /preview for a downsampled view.

The calibration firewall. A likelihood ratio is valid only when the score was produced under the same scorer config as the reference. /v1/steps/calibrate — and a scorer_config override on /v1/compare — refuse to emit a calibrated LR when the config hashes disagree, returning the raw score with calibrated: false rather than a mis-scaled number.

GET/v1/scorer-config
The deployed scorer hyperparameters + their content hash.
GET/v1/references
Every calibration reference’s scorer hash, source datasets, and Cllr/Cllrmin/AUC — exactly what each LR is calibrated on.
POST/v1/steps/calibrate
Map a score → bounded LR against a reference; refuses an off-config score (the firewall).

Clients. Thin Python and R clients wrap the API; reproduce() re-runs a comparison and checks the handle matches.

from verity_client import VerityClient        # clients/python
v = VerityClient("https://api.verity.codes")

r = v.compare("impressed", "breech_a.x3p", "breech_b.x3p")
print(r["likelihood_ratio"], r["handle"])     # the content address

# verify a published LR — a one-line hash-equality check
assert v.reproduce("impressed", "breech_a.x3p", "breech_b.x3p",
                   expect_handle=r["handle"])

Clients: clients/

The format

X3P codec

Verity reads and writes X3P (ISO 25178-72) — the open container for 3-D surface topography — through a single native Rust core, verity-x3p, with thin per-language bindings. A file written from any binding reads back bit-identically in every other. Heights are an (ny, nx) matrix of f64 (invalid points are NaN) with a parallel validity mask and the X3P axis/provenance metadata.

use verity_x3p::{read_x3p, write_x3p, WriteOptions};

let surface = read_x3p("scan.x3p")?;          // verifies the stored MD5
println!("{} x {} points", surface.nx(), surface.ny());
write_x3p(&surface, "copy.x3p", &WriteOptions::default())?;

Install: pip install verity-x3p (PyPI, abi3 wheels, Python 3.9+) or cargo add verity-x3p(crates.io) — both v0.2.0. The R binding isn’t on CRAN: R CMD INSTALL bindings/r/verityx3p from a clone, which needs a Rust toolchain. TypeScript / Swift / Java bindings are planned.

For examiners

Verity accepts 3-D surface-topography scans in X3P (ISO 25178-72). To resolve the individualizing roughness band the calibration relies on, the lateral pitch must be fine enough — ≤ 2 µm (Nyquist for the λs= 4 µm short cutoff); a scan coarser than 4 µm is refused rather than scored. Every comparison is checked against its reference’s validated domain (resolution, mark type, coverage, signal) before an LR is emitted. The result is a calibrated weight of evidence — one input to an examiner’s judgment, not a verdict, and not a claim about the error rate of examination.

The data

Data catalog

verity-catalogis a normalized catalog + content-addressed (SHA-256) store + manifest-driven ingestion for forensic X3P scans harvested from NIST NBTRD (U.S. public domain), CSAFE-ISU’s cartridge-case scans on Figshare (CC BY 4.0), and the tmaRks toolmark set (MIT). It is local-first (SQLite + a blob directory, no external services) and scales to Postgres + object storage by setting VERITY_CATALOG_* env vars — no code change. Same-source (KM/KNM) labels fall out of the Study → Firearm → Bullet → Land → Scan hierarchy, and every scan is validated with verity-x3p on ingest.

bash
uv run verity-catalog init-db
uv run verity-catalog ingest hamby252-barrel1-sample --limit 2  # ~2.9 MB scans from NBTRD
uv run verity-catalog info
How it fits

Architecture

Verity is a polyglot monorepo: one Rust codec core, thin language bindings, and the Python science + service stack on top.

PackageLangRole
crates/verity-x3pRustNative X3P reader/writer — the format’s single source of truth.
bindings/python · bindings/rPyO3 · extendrThin wrappers over the core for bit-identical I/O.
services/enginePythonMetrology preprocessing, registration, CMR, the calibrated-LR decision layer.
services/apiFastAPIThe comparison HTTP API serving the ComparisonReport.
services/catalogPythonCatalog + content-addressed store + ingestion.
services/webNext.jsThis site and the interactive comparison UI.
services/mcpPythonMCP server (“verity”) — local stdio plus the hosted endpoint at api.verity.codes/mcp.

The firewall. A learned or hand-engineered representation produces a score; a transparent, empirically-capped calibration turns that score into the reportable LR. The report is interpretable regardless of how the score was computed — the defense against the black box.

Where things stand

Status & roadmap

Verity is a research preview — the engine and API are version 0.1.0. The method, the validation protocols, and the code are open and reproducible, but Verity is not a production forensic system, and its output should not be the basis of a casework or courtroom decision.

What ships today. Three calibrated mark types (striated bullet lands, impressed breech faces, striated toolmarks), each scoped to a named reference population; the monotone, empirically-capped LR firewall; the glass-box step API for auditing every intermediate; and the open benchmark with frozen, source-disjoint splits. The verity-x3p codec is published at v0.2.0 on crates.io and PyPI. Every number on this site is labeled with the protocol that produced it.

What’s ahead. Broader reference populations and cross-instrument validation, external replication through the open benchmark, versioned releases of the engine, and independent review. Follow progress on GitHub.

The pre-registered external test landed — H1 supported. The one-shot validation on untouched data was pre-registered on OSF (2026-07-01, before any data access) and run once as registered: on the independent Weller et al. (2012) cartridge-case set the frozen pipeline reached pooled Cllr 0.163 (95% CI 0.147–0.189), AUC 0.9995 — the pre-registered H1 (pooled Cllr ≤ 0.45) is supported. Protocol: docs/weller-preregistration.md.

Pointers

Reference

Cite Verity

Use GitHub’s “Cite this repository” button, backed by CITATION.cff. The preferred citation is the Verity whitepaper.

Licensed MIT / Apache-2.0. Verity reports a calibrated weight of evidence; it never makes the decision, and makes no claim about the error rate of forensic examination, which remains unknown.