Chio/Docs
LOGIN · JOIN

EconomyCredit

Underwriting Risk Taxonomy

Underwriting produces signed credit decisions from receipt history, reputation, certifications, and runtime attestations.

Underwriting turns a window of receipt history into a signed credit decision. The taxonomy below is the vocabulary that decision is written in: the risk classes an evaluator can assign, the reason codes it cites, the evidence kinds each reason points back to, and the policy thresholds that turn a set of signals into an outcome.

Implementation and schema

The taxonomy, reason codes, and evidence kinds live in crates/economy/chio-underwriting/src/lib.rs; the decision policy, decision report, signed decision record, and premium quote live in crates/economy/chio-underwriting/src/decision.rs; the standalone insurance-flow premium function lives in crates/economy/chio-underwriting/src/premium.rs. Names and defaults below match those files exactly.

Risk classes

UnderwritingRiskClass tags every finding with one of four severity levels. The class is ordered: each higher level dominates the lower ones when rolling findings up to the report-level risk_class.

ClassReadingTypical decision
BaselineClean evidence, no findings against itApprove at requested ceiling
GuardedOne or more soft signals (probationary history, weaker runtime assurance than the approve target but above the step-up floor)Approve with reduced ceiling
ElevatedMaterial signals (stale or thin receipt history, low reputation below the approve threshold, missing runtime assurance)Reduce ceiling or step up for manual review
CriticalHard failures (failed or revoked certification, failed settlement exposure, reputation below the deny threshold)Deny

The taxonomy is versioned. Each signed decision record embeds the taxonomy version (chio.underwriting.taxonomy.v1) so consumers can audit which schema produced a given outcome.


Reason codes

UnderwritingReasonCode is the machine-readable label attached to each input UnderwritingSignal. Thirteen codes are defined. The table below gives the trigger condition, the risk severity that signal carries when present, and the typical decision the evaluator routes it to.

CodeTriggerSeverityTypical routing
ProbationaryHistorySubject is still in the probationary window of its reputation tierGuardedReduceCeiling
LowReputationEffective score below the approve threshold (or below the deny threshold)Elevated or CriticalReduceCeiling or Deny
ImportedTrustDependencyReputation depends on signals imported from another peerGuardedReduceCeiling
MissingCertificationTool requires a certification record that was not foundElevatedStepUp (when policy requires certs)
FailedCertificationCertification check ran and returned a failing verdictCriticalDeny
RevokedCertificationCertification was previously valid but has been revokedCriticalDeny
MissingRuntimeAssuranceNo runtime attestation observed for governed receiptsElevatedStepUp
WeakRuntimeAssuranceRuntime assurance tier is below the approve target but above the step-up floorGuardedReduceCeiling
PendingSettlementExposureOutstanding receipts are not yet settledGuarded or ElevatedReduceCeiling
FailedSettlementExposureOne or more prior settlements failedCriticalDeny
MeteredBillingMismatchMetered usage and billed amount disagreeGuarded or ElevatedReduceCeiling
DelegatedCallChainCall originated through a delegation chain that adds riskGuardedReduceCeiling
SharedEvidenceProofRequiredEvidence references shared sources that need additional proofGuardedReduceCeiling

These reason codes are the input vocabulary. The evaluator translates them into UnderwritingDecisionReasonCode values on findings (for example, PolicySignal, ReputationBelowApproveThreshold, RuntimeAssuranceBelowStepUpTier), which carry the original signal reason on the finding so consumers can trace the decision back to its evidence.


Decision outcomes

UnderwritingDecisionOutcome has four ordered variants. The report-level outcome is the maximum of every finding's outcome, so any single deny finding wins.

OutcomeMeaningMaps to UnderwritingReviewStateMaps to UnderwritingBudgetAction
ApproveGrant the requested ceiling at full strengthApprovedPreserve
ReduceCeilingGrant a narrower ceiling, multiplied by reduce_ceiling_factorApprovedReduce
StepUpHold pending manual review or stronger evidenceManualReviewRequiredHold
DenyReject; no ceiling is grantedDeniedDeny

Default policy thresholds

UnderwritingDecisionPolicy::default() ships with the values below. The policy is signed alongside the decision report so the thresholds in force at decision time are recoverable from the record.

crates/economy/chio-underwriting/src/decision.rsrust
pub struct UnderwritingDecisionPolicy {
    pub schema: String,
    pub version: String,
    pub minimum_receipt_history: u64,
    pub maximum_receipt_age_seconds: u64,
    pub minimum_approve_reputation_score: f64,
    pub deny_reputation_score_below: f64,
    pub minimum_step_up_runtime_assurance_tier: RuntimeAssuranceTier,
    pub minimum_approve_runtime_assurance_tier: RuntimeAssuranceTier,
    pub require_active_tool_certification: bool,
    pub require_compliance_score_reference: bool,
    pub reduce_ceiling_factor: f64,
}

impl Default for UnderwritingDecisionPolicy {
    fn default() -> Self {
        Self {
            schema: UNDERWRITING_DECISION_POLICY_SCHEMA.to_string(),
            version: UNDERWRITING_DECISION_POLICY_VERSION.to_string(),
            minimum_receipt_history: 1,
            maximum_receipt_age_seconds: 60 * 60 * 24 * 30,
            minimum_approve_reputation_score: 0.6,
            deny_reputation_score_below: 0.25,
            minimum_step_up_runtime_assurance_tier: RuntimeAssuranceTier::Attested,
            minimum_approve_runtime_assurance_tier: RuntimeAssuranceTier::Verified,
            require_active_tool_certification: true,
            require_compliance_score_reference: false,
            reduce_ceiling_factor: 0.5,
        }
    }
}
Reputation scoreRouted to
score >= 0.6Approve (subject to other findings)
0.25 <= score < 0.6ReduceCeiling on a ReputationBelowApproveThreshold finding (manual review when combined with other elevated signals)
score < 0.25Deny on a ReputationBelowDenyThreshold finding

policy.validate() rejects configurations that put the deny floor at or above the approve threshold, so the manual review band cannot collapse to zero width.

These thresholds belong to underwriting alone. The credit scorecard has its own vocabulary, five bands from Prime to Restricted, and nothing derives one from the other: the credit facility record carries an underwriting outcome, a review state and a risk class as fields beside a scorecard, and no function in either crate computes a band from a threshold or a threshold from a band. See Credit Scorecards for what actually sets a band.

Lookback windows

Receipt evidence freshness is governed by maximum_receipt_age_seconds on the policy (default 30 days). A separate lookback window is used by price_premium for premium pricing, expressed as a LookbackWindow{ since, until } and recorded on the resulting PremiumQuote for audit.

Evidence kinds and compliance bundle

Findings reference UnderwritingEvidenceReference records, each tagged with an UnderwritingEvidenceKind:

crates/economy/chio-underwriting/src/lib.rsrust
pub enum UnderwritingEvidenceKind {
    Receipt,
    ReputationInspection,
    CertificationArtifact,
    RuntimeAssuranceEvidence,
    SettlementReconciliation,
    MeteredBillingReconciliation,
    SharedEvidenceReference,
}

These kinds let consumers route evidence back to the system that produced it. A RuntimeAssuranceEvidence reference, for example, includes a SHA-256 digest and a verifier locator pointing at the attestation that justified the finding.

UnderwritingComplianceEvidence

When the optional compliance score is included in the input bundle, it ships in this fixed shape, with score on the same 0 to 1000 scale as the kernel compliance score and generated_at in unix seconds:

crates/economy/chio-underwriting/src/lib.rs275-286rust
pub struct UnderwritingComplianceEvidence {
    pub schema: String,
    pub agent_id: String,
    pub score: u32,
    pub generated_at: u64,
    pub total_receipts: u64,
    pub deny_receipts: u64,
    pub observed_capabilities: u64,
    pub revoked_capabilities: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub attestation_age_secs: Option<u64>,
}

When require_compliance_score_reference is set on the policy and this field is None, the evaluator emits a ComplianceScoreRequired finding that routes to StepUp.

Imported evidence inputs

The policy input at schema chio.underwriting.policy-input.v1 carries four optional evidence blocks alongside the receipt evidence: reputation, certification, runtime assurance, and the compliance score. Only the reputation block admits evidence that originated on another node. It reports how many trust signals arrived from elsewhere in importedSignalCount and how many the local behavioral feed accepted in acceptedImportedSignalCount.

An accepted count above zero does not raise the subject. The evaluator emits an ImportedTrustDependency signal in class Guarded, naming the count and referring back to the reputation inspection it came from. A subject leaning on imported standing is a subject to look at more closely, not one with standing of its own. See Portable Reputation for how a signal crosses nodes and what the receiving feed does with it.


Premium pricing

Two premium interfaces ship with chio-underwriting: the decision-time premium quoted in the signed record, and the standalone price_premium function used by chio-market to quote-and-bind an insurance policy.

Decision-time Premium

Each signed UnderwritingDecisionArtifact carries an UnderwritingPremiumQuote:

crates/economy/chio-underwriting/src/decision.rsrust
pub enum UnderwritingPremiumState {
    Quoted,
    Withheld,
    NotApplicable,
}

pub struct UnderwritingPremiumQuote {
    pub state: UnderwritingPremiumState,
    pub basis_points: Option<u32>,
    pub quoted_amount: Option<MonetaryAmount>,
    pub rationale: String,
}

Basis points (a basis point is 1/100 of a percent, so 100 bps = 1%) are a fixed schedule keyed off the outcome and the rolled-up risk class:

OutcomeBaselineGuardedElevatedCritical
Approve100 bps150 bps200 bps300 bps
ReduceCeiling150 bps250 bps400 bps600 bps
StepUpWithheld (no quote until manual review or stronger evidence completes)
DenyNotApplicable

The quoted amount equals ceil(exposure_units * bps / 10_000) in the same currency as the quoted exposure. When no exposure is supplied, only the basis points appear on the quote.

Insurance-flow Premium

The standalone price_premium function takes a compliance score in 0..=1000 plus an optional behavioral z-score and produces a PremiumQuote. It is deterministic and fail-closed: a missing compliance score returns a decline instead of silently approving.

crates/economy/chio-underwriting/src/premium.rs3-18rust
//! `price_premium` turns a 0..=1000 compliance score and optional behavioral
//! anomaly signal into a deterministic insurance premium quote. The formula
//! is:
//!
//! ```text
//! quoted_cents = base_rate_cents * (1 + risk_multiplier(score))
//! ```
//!
//! where `risk_multiplier` is a stepwise function of the combined score:
//!
//! | Score band  | Multiplier | Disposition |
//! |-------------|-----------:|-------------|
//! | `> 900`     |        1.0 | quoted      |
//! | `700..=900` |        2.0 | quoted      |
//! | `500..700`  |        5.0 | quoted      |
//! | `< 500`     |          - | declined    |

Behavioral anomalies erode the score through a per-sigma penalty before the band is chosen. Defaults: penalty per sigma above the threshold is 50 points, capped at 250 points total, with the threshold at 3 sigma. The quote's justification string records the inputs used for audit.

The behavioral z-score is populated from chio_kernel::behavioral_anomaly_score. The signed insurer-facing feed that reports it for premium pipeline is chio trust behavioral-feed export (HTTP GET /v1/reports/behavioral-feed), which exports the behavioral-anomaly signal consumed as PremiumInputs.behavioral_z_score.

See Liability Market for how this quote is bound into a policy via quote_and_bind.


Worked example

Take an agent with two recent governed receipts, a Verified runtime assurance tier, and a reputation score of 0.4. The agent is still in its probationary window. The input carries one signal: ProbationaryHistory at Guarded severity.

example.rsrust
UnderwritingPolicyInput {
    schema: "chio.underwriting.policy-input.v1",
    generated_at: 1_700_000_000,
    filters: UnderwritingPolicyInputQuery {
        agent_subject: Some("agent-42".into()),
        receipt_limit: Some(100),
        ..default()
    },
    taxonomy: UnderwritingRiskTaxonomy::default(),
    receipts: UnderwritingReceiptEvidence {
        matching_receipts: 2,
        // ... two recent governed receipts ...
    },
    reputation: Some(UnderwritingReputationEvidence {
        subject_key: "agent-42".into(),
        effective_score: 0.4,
        probationary: true,
        ..
    }),
    runtime_assurance: Some(UnderwritingRuntimeAssuranceEvidence {
        highest_tier: Some(RuntimeAssuranceTier::Verified),
        ..
    }),
    signals: vec![UnderwritingSignal {
        class: UnderwritingRiskClass::Guarded,
        reason: UnderwritingReasonCode::ProbationaryHistory,
        description: "subject is still probationary".into(),
        evidence_refs: vec![/* ... */],
    }],
    ..default()
}

The evaluator produces two findings:

  • A reputation finding: score 0.4 is below the approve threshold of 0.6 and above the deny floor of 0.25, so it routes to ReduceCeiling at Elevated class with reason ReputationBelowApproveThreshold and signal_reason LowReputation.
  • A signal-driven finding for ProbationaryHistory: routes to ReduceCeiling at Guarded class with reason PolicySignal.

Rolled up: report outcome is ReduceCeiling (the maximum of the two), risk class is Elevated (the maximum of the two), and suggested_ceiling_factor is 0.5 from the default policy. The signed decision record carries:

  • review_state: Approved
  • budget: Reduce with ceiling factor 0.5
  • premium: Quoted at 400 bps (ReduceCeiling at Elevated). MonetaryAmount.units are integer minor units, so a $10.00 exposure is units: 1_000; the quoted premium is ceil(1000 * 400 / 10_000) = 40 minor units ($0.40) in the same currency.

Worked decision example

Consider an agent with reputation 0.42, eight governed receipts in the last 30 days, no tool-server certification on file. The operator wants to size a 1,000-unit ceiling against this agent and scopes the evaluation to a specific tool server. Walk through what the evaluator emits.

Reputation evidence shape

The reputation block on UnderwritingPolicyInput is fixed:

crates/economy/chio-underwriting/src/lib.rsrust
pub struct UnderwritingReputationEvidence {
    pub subject_key: String,
    pub effective_score: f64,
    pub probationary: bool,
    pub resolved_tier: Option<String>,
    pub imported_signal_count: usize,
    pub accepted_imported_signal_count: usize,
}

For our agent this resolves to:

example-input.jsonjson
{
  "subjectKey": "agent-vanguard-soc2",
  "effectiveScore": 0.42,
  "probationary": false,
  "resolvedTier": "tier-2",
  "importedSignalCount": 0,
  "acceptedImportedSignalCount": 0
}

Triggers

Two thresholds fire on this input:

  • LowReputation: 0.42 is below the approve threshold of 0.6 and above the deny floor of 0.25, so it routes to a ReputationBelowApproveThreshold finding at Elevated class.
  • MissingCertification: the policy default require_active_tool_certification is true, the query names a tool server, and no certification record resolves for it. The finding routes at Elevated class with outcome StepUp and reason PolicySignal. The certification arm only fires when the query is scoped to a tool server; an unscoped evaluation skips it entirely.

Outcome

The reputation finding carries ReduceCeiling; the certification finding carries StepUp. Eight receipts is enough history and the runtime assurance tier clears the approve target, so neither history nor runtime adds a finding. Because UnderwritingDecisionOutcome is ordered Approve < ReduceCeiling < StepUp < Deny and the report outcome is the maximum over every finding, the StepUp certification finding dominates. Rolled up: report outcome is StepUp, risk class is Elevated (the max of the two Elevated classes). Each UnderwritingDecisionFinding records its own reason (a UnderwritingDecisionReasonCode) plus an optional signal_reason that carries the originating input signal for traceability: ReputationBelowApproveThreshold with signal reason LowReputation on the first, PolicySignal with signal reason MissingCertification on the second.

Premium calculation

A StepUp outcome withholds the premium. The signed UnderwritingDecisionArtifact includes an UnderwritingPremiumQuote with state Withheld, no basis_points, and no quoted_amount; nothing is priced until manual review or stronger evidence clears the step-up.

Had the certification resolved and this decision landed at ReduceCeiling at Elevated instead, the schedule prices that pair at 400 bps and the amount comes from the chio-underwriting helper:

crates/economy/chio-underwriting/src/decision.rs862-876rust
fn quote_premium_amount(
    exposure: &MonetaryAmount,
    basis_points: u32,
) -> Result<MonetaryAmount, String> {
    let numerator = u128::from(exposure.units)
        .checked_mul(u128::from(basis_points))
        .ok_or_else(|| "premium amount multiplication overflowed".to_string())?;
    let units = numerator.div_ceil(10_000_u128);
    let units = u64::try_from(units)
        .map_err(|_| "premium amount exceeds the supported minor-unit range".to_string())?;
    Ok(MonetaryAmount {
        units,
        currency: exposure.currency.clone(),
    })
}

Apply it: units = ceil(1000 * 400 / 10_000) = 40. That counterfactual quote would carry basis_points = 400 and quoted_amount = MonetaryAmount{ units: 40, currency: "USD" }. The helper returns a Result, not a saturated amount: a multiplication that overflows u128, and a product that no longer fits u64 minor units, are both errors rather than a silently truncated or wrapped premium. The test quote_premium_amount_rejects_when_basis_points_force_overflow pins that.

Running it

The evaluation runs through chio trust underwriting-decision evaluate, reading receipts from a local database (or a trust-control service via --control-url / --control-token):

economy · underwriting-evaluatetranscript
$ chio trust underwriting-decision evaluate \
    --agent-subject demo-agent \
    --tool-server ts-demo \
    --receipt-db ./receipts.db
schema:                 chio.underwriting.decision-report.v1
generated_at:           1788540481
outcome:                Deny
risk_class:             Critical
policy_version:         chio.underwriting.decision-policy.default.v1
matching_receipts:      0
findings:               4
- StepUp InsufficientReceiptHistory: only 0 receipt(s) matched; policy requires at least 1
- Deny ReputationBelowDenyThreshold: effective reputation score 0.0000 is below the deny threshold 0.2500
- ReduceCeiling PolicySignal: local reputation is still probationary for the requested window
- StepUp PolicySignal: no active certification evidence is available for tool server `ts-demo`
exit 0

That run has no receipt history at all, which is the honest worst case and shows the default policy failing closed. Zero matching receipts trips the history minimum, a reputation score of zero falls below the deny threshold, the subject is still probationary, and no certification resolves for the tool server. Four findings, and the strongest of them sets the outcome.

evaluate is read-only: it returns the unsigned UnderwritingDecisionReport (schema, outcome, risk class, policy version, suggested ceiling factor, matching receipt count, and one line per finding) and never persists anything. The report itself has no decision_id, review_state, budget, or premium field. To mint the signed record that downstream consumers verify, run chio trust underwriting-decision issue instead. Its output adds the decision identity and the fields that only exist on UnderwritingDecisionArtifact:

economy · underwriting-issuetranscript
$ chio trust underwriting-decision issue \
    --agent-subject demo-agent \
    --tool-server ts-demo \
    --receipt-db ./receipts.db \
    --authority-seed-file ./authority.seed
schema:                 chio.underwriting.decision.v1
decision_id:            uwd-194242790e6dd4670371d2e60cae09d0585615993e4636a6c92cee20b21977f8
issued_at:              1788540482
signer_key:             a304e5344fc024ba2244dc108c104558107c34f776cbfdef8d256ad414f6c6b1
outcome:                Deny
review_state:           Denied
budget_action:          Deny
premium_state:          NotApplicable
exit 0

The signer is the node's behavioral-feed signing keypair, loaded from --authority-seed-file or --authority-db. It is loaded before the report is built, because the same public key anchors the trusted-key set that reputation scoring runs against, and that scoring fails closed on an empty set. Without a seed or a database the command refuses rather than signing with an ephemeral key.

evaluate and issue share the same evidence filters (--capability, --tool-server, --tool-name, --since / --until, --receipt-limit). A third verb, simulate, replays an alternative policy file against the same evidence without persisting a decision.


Appeals

Decisions can be challenged via an UnderwritingAppealRecord. Appeals carry a status of Open, Accepted, or Rejected. An accepted appeal can reference a replacement_decision_id; creating that replacement supersedes the original decision (lifecycle state moves from Active to Superseded).


Next steps

  • Scorecards : the credit-scorecard view that pairs with the underwriting input bundle.
  • Credit Facilities : how an underwriting decision turns into a granted facility and a collateral bond.
  • Liability Market : provider types, jurisdiction policies, and quote-and-bind mechanics.
  • Claims Lifecycle : the seven-stage workflow from package to settlement receipt.
  • Credit & Underwriting Guide : walkthrough that connects this taxonomy to the CLI.