Risk scoring
The risk score is a pure, rule-based function — additive, capped at 100. There is no ML anywhere in this path (the synthetic-media score is a separate, external signal). Given a bundle, the device record, and whether the image bytes matched, it returns a score and the factors that produced it.
Factors
| Factor | Points | Condition |
| --- | --- | --- |
| no_manifest | 60 | No attestation bundle found in the asset |
| signature_invalid | 70 | Signature fails ECDSA verification |
| sensor_hash_mismatch | 50 | Image bytes hash ≠ bundle sensor_hash |
| device_revoked | 80 | Device in registry, marked revoked |
| device_not_enrolled | 20 | Device not in registry |
| cert_unknown_ca | 40 | Cert not signed by a known root CA |
| timestamp_stale | 25 | capture_timestamp > 72h before claim submission |
| gps_absent | 30 | gps null or lat/lng both 0 |
| biometric_absent | 10 | biometric_present = false |
| video_manifest_incomplete | 40 | Video: chunk indices non-contiguous, count mismatch, or missing fields |
| frame_hash_mismatch | 50 | Video: recomputed chunk chain ≠ manifest (the video analogue of sensor-hash mismatch) |
| session_nonce_mismatch | 70 | Live only: bundle nonce ≠ server nonce, or chain doesn't seed from it — the primary anti-replay check |
| liveness_check_failed | 40 | Live only: secondary timing/continuity heuristics (not biometric liveness) |
| hardware_backing_absent (native hardware SDK) | 40 | Enrolled device whose server-verified hardware_tier is software — real hardware that couldn't prove enclave backing. Sandbox simulated devices are exempt |
| exif_timestamp_mismatch | 15 | JPEG EXIF DateTimeOriginal differs from the attestation time beyond tolerance (default 10 min, EXIF_TOLERANCE_SECONDS) |
| exif_editor_trace | 15 | JPEG EXIF Software tag names an image editor (Photoshop, GIMP, Lightroom…) |
| exif_missing | 5 | A JPEG capture with no EXIF metadata (stripped) — weak signal |
| geofence_mismatch | 20 | Attestation GPS is farther than the geofence radius (default 5 km, GEOFENCE_RADIUS_KM) from the claim's stated loss_location |
| cross_claim_link | 15 / 30 | This claim shares a device, location, or cloned cert with another of the carrier's claims. +15 for a weak link (GPS proximity), +30 for a strong one (shared device / cloned cert); the factor caps its own contribution at 30 |
The last four EXIF/geofence rows are advisory metadata signals: modest weights, all thresholds env-tunable, and they only fire when the input exists — EXIF factors apply to JPEGs only, and geofence never fires without a stated loss_location (a missing input is never penalized). Pass loss_location: { lat, lng } on attest to enable the geofence check.
Thresholds
| Score | Status | Action |
| --- | --- | --- |
| 0–30 | verified | Auto-approve |
| 31–60 | partial | Adjuster review |
| 61–80 | partial | Enhanced review |
| 81–100 | failed | SIU escalation |
What to do at each factor
signature_invalid/sensor_hash_mismatch/frame_hash_mismatch— the content was altered after signing. Treat as fraud until proven otherwise.device_not_enrolled/no_manifest— a legacy or web upload that never touched the SDK. Consider screening.device_revoked/cert_unknown_ca— a device or CA problem; check the device registry.gps_absent/biometric_absent/timestamp_stale— soft signals; they nudge, they don't condemn.exif_timestamp_mismatch/exif_editor_trace/exif_missing/geofence_mismatch— cheap metadata cross-checks an adjuster would otherwise do by hand; advisory, never a hard failure.cross_claim_link: this claim shares signals with another claim from the same carrier. See cross-claim links. Advisory; only affects new verifications (stored scores aren't retroactively rescored — the links panel is the retroactive view).
Carrier-configurable thresholds & weights
The 30/80 cuts and per-factor weights above are defaults, not law. A carrier can publish a risk policy (the claims_risk policy domain) that tunes them per its own risk appetite and jurisdiction — without any code change and without retroactively rescoring stored claims.
A risk policy has three optional parts:
thresholds— override theverified/partialcutoffs. Bounded:0 < verified < partial < 100. "Our SIU threshold is 60" becomes{ thresholds: { verified: 20, partial: 60 } }.factor_weights— retune any factor's points, keyed by its code. Each weight is clamped to[0, 2×default]so a policy can dampen or emphasize a signal but never invent an unbounded one.rules— required-signal rules over claim facts (claim_type,state,gps_present,biometric_present,attested) using the same condition DSL as intent policies. When a rule matches and its required signal is absent, an advisoryCARRIER_POLICY_<SIGNAL>_MISSINGfactor is added. It raises the score and never fails the attestation on its own. "In FL, water-damage claims need GPS":
{
"thresholds": { "verified": 20, "partial": 60 },
"factor_weights": { "carrier_policy_gps_missing": 40 },
"rules": [
{ "id": "fl-water-gps",
"when": [{ "fact": "claim_type", "op": "eq", "value": "water" },
{ "fact": "state", "op": "eq", "value": "FL" }],
"require": { "signal": "gps" } }
]
}
Attest with claim_type / state so the rules can match. A carrier with no risk policy scores exactly as this page describes — bit-for-bit (the config is an optional argument to a pure scorer, regression-locked). Preview a draft against sample facts with POST /v1/policy/simulate?domain=claims_risk. Edit it in the dashboard's Policies → Claims risk tab.
The two-signal rule
risk_score and synthetic_media_score are never merged. An adjuster reads them together:
risk_score= "can we prove this is authentic?" — cryptographic, deterministic.synthetic_media_score= "does an external detector think this looks AI-generated?" — probabilistic, advisory.
When the first signal is confident (a valid signature and matching hash), the second is irrelevant. The second only earns attention when the first can't vouch — e.g. a no_manifest legacy upload. Merging them into one number would blur a proof with a guess, which is exactly what Provenance refuses to do.