Underwriting
Size an agent's spending ceiling from its record, then read the decision's reasons, risk class, and premium.
On this page
Underwriting decides how much of a requested spending ceiling an agent keeps. The evaluator reads the agent's receipts, reputation, runtime assurance, and certification under a signed policy, and records one outcome with every reason behind it and the premium it implies.
The result is a signed decision record that a credit facility or a liability policy consumes; the record itself issues no capability and moves no funds.
A probationary agent keeps half its ceiling
Reputation 0.4 sits between the 0.25 deny floor and the 0.6 approve threshold, and a ProbationaryHistory signal fires. The report takes the most restrictive finding, ReduceCeiling at Elevated, with ceiling factor 0.5 and a 400 basis point premium: USD 0.40 on a USD 10.00 exposure. Underwriting.
UnderwritingDecisionPolicy, rolling up to one UnderwritingDecisionOutcome; the reputation score and premium follow the worked example and are illustrative. The signed decision record issues no capability and moves no funds.The most restrictive finding sets the outcome
Each piece of evidence produces a finding with an outcome and a risk class. Outcomes are ordered Approve < ReduceCeiling < StepUp < Deny and risk classes Baseline < Guarded < Elevated < Critical. The report takes the maximum of each, so a single deny finding wins, and the signed record maps the outcome to a review state and a budget action:
Approvekeeps the requested ceiling at full strength:Approved,Preserve.ReduceCeilingmultiplies the ceiling byreduce_ceiling_factor:Approved,Reduce.StepUpholds the request for manual review or stronger evidence:ManualReviewRequired,Hold.Denygrants no ceiling:Denied,Deny.
Every UnderwritingDecisionFinding keeps its own reason, an UnderwritingDecisionReasonCode, and an optional signal_reason naming the input signal that raised it, so a consumer can trace the outcome back to its evidence. The taxonomy is versioned: each signed record embeds chio.underwriting.taxonomy.v1.
Trace a step-up: reputation 0.42 and a missing certification
A month after Quickstart part 2, you size a USD 10.00 quote ceiling (1,000 minor units) for an outside buyer's agent, scoped to the pricing tool server. The buyer has reputation 0.42, eight governed receipts in the last 30 days, and no tool-server certification on file. The reputation block resolves to this:
{
"subjectKey": "did:chio:z6MkbY4…e1R",
"effectiveScore": 0.42,
"probationary": false,
"resolvedTier": "tier-2",
"importedSignalCount": 0,
"acceptedImportedSignalCount": 0
}Two findings fire. 0.42 sits between the 0.25 deny floor and the 0.6 approve threshold, so LowReputation raises a ReputationBelowApproveThreshold finding with ReduceCeiling at Elevated. The policy default require_active_tool_certification is true, the query names a tool server, and no certification resolves for it, so a PolicySignal finding with signal reason MissingCertification routes to StepUp at Elevated. The certification check runs only when the query is scoped to a tool server.
Eight receipts are enough history and the runtime assurance tier clears the approve target, so neither adds a finding. StepUp dominates: the report outcome is StepUp at Elevated, and the premium quote is Withheld, with no basis_points and no quoted_amount, until review or stronger evidence clears it.
A probationary agent keeps half its ceiling
Go back to the buyer's first week: two governed quotes, a Verified runtime assurance tier, and a reputation score of 0.4, still inside its probationary window. These are selected fields of its policy input; the complete input also carries the taxonomy, receipt counters, and evidence references.
{
"schema": "chio.underwriting.policy-input.v1",
"generatedAt": 1700000000,
"filters": { "agentSubject": "did:chio:z6MkbY4…e1R", "receiptLimit": 100 },
"receipts": { "matchingReceipts": 2 },
"reputation": {
"subjectKey": "did:chio:z6MkbY4…e1R",
"effectiveScore": 0.4,
"probationary": true
},
"runtimeAssurance": { "highestTier": "verified" },
"signals": [{
"class": "guarded",
"reason": "probationary_history",
"description": "subject is still probationary"
}]
}The evaluator produces two findings. The reputation finding routes 0.4 to ReduceCeiling at Elevated, with reason ReputationBelowApproveThreshold and signal reason LowReputation. The ProbationaryHistory signal routes to ReduceCeiling at Guarded, with reason PolicySignal.
Rolled up, the outcome is ReduceCeiling at Elevated, with suggested_ceiling_factor 0.5 from the default policy. The signed record carries review_state Approved, budget Reduce with factor 0.5, and a premium quoted at 400 basis points. Approved records the review; Reduce records that the buyer keeps half the requested ceiling. MonetaryAmount.units are integer minor units, so a $10.00 exposure is units: 1_000 and the premium is ceil(1000 * 400 / 10_000) = 40: $0.40 in the same currency.
The default policy routes reputation into three bands
UnderwritingDecisionPolicy::default() requires at least one matching receipt, the newest at most 30 days old (maximum_receipt_age_seconds). Runtime assurance must reach Attested to avoid a step-up and Verified to approve, an active tool certification is required, a compliance score reference is optional, and reduce_ceiling_factor is 0.5. Reputation routes by two thresholds:
score >= 0.6approves, subject to the other findings.0.25 <= score < 0.6raises aReputationBelowApproveThresholdfinding and reduces the ceiling, or steps up when other elevated signals join it.score < 0.25raises aReputationBelowDenyThresholdfinding and denies.
The policy is signed alongside the decision report, so the thresholds in force at decision time are recoverable from the record. policy.validate() rejects a deny floor at or above the approve threshold, which keeps the reduce band wider than zero.
These thresholds belong to underwriting alone. The credit scorecard has its own five bands, from Prime to Restricted. A credit facility record carries an underwriting outcome, review state, and risk class as fields beside a scorecard, and no function in either crate derives a band from a threshold or a threshold from a band.
Evaluate reads the evidence; issue signs the decision
Run the evaluation with chio trust underwriting-decision evaluate. It reads receipts from a local database, or from a trust control plane with --control-url and --control-token.
$ chio trust underwriting-decision evaluate \
--agent-subject demo-agent \
--tool-server ts-demo \
--receipt-db ./receipts.dbschema: 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`
You should see the default policy fail closed on an empty history. Zero matching receipts trip 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 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 persists nothing; decision_id, review_state, budget, and premium exist only on the signed record. chio trust underwriting-decision issue mints that UnderwritingDecisionArtifact:
$ chio trust underwriting-decision issue \
--agent-subject demo-agent \
--tool-server ts-demo \
--receipt-db ./receipts.db \
--authority-seed-file ./authority.seedschema: 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
The signer is the node's behavioral-feed signing keypair, loaded from --authority-seed-file or --authority-db before the report is built. The same public key anchors the trusted-key set that reputation scoring runs against, and that scoring fails closed on an empty set, so without a seed or a database the command refuses to sign with an ephemeral key.
Both verbs share the evidence filters --capability, --tool-server, --tool-name, --since, --until, and --receipt-limit. A third verb, simulate, replays an alternative policy file against the same evidence without persisting a decision.
A decision can be challenged with an UnderwritingAppealRecord, whose status is Open, Accepted, or Rejected. An accepted appeal can name a replacement_decision_id; creating that replacement moves the original decision's lifecycle state from Active to Superseded. An operator opens an appeal with chio trust underwriting-appeal create --decision-id --requested-by --reason and closes it with chio trust underwriting-appeal resolve --appeal-id --resolution accepted|rejected --resolved-by (chio trust).
Risk taxonomy, policy, and evidence inputs
Search for MissingRuntimeAssurance, reduce_ceiling_factor, or price_premium.
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 in crates/economy/chio-underwriting/src/decision.rs; the insurance-flow premium function in crates/economy/chio-underwriting/src/premium.rs.
Risk classes
UnderwritingRiskClass tags every finding. Each higher class dominates the lower ones when findings roll up to the report's risk_class.
| Class | Reading | Typical decision |
|---|---|---|
Baseline | Clean evidence, no findings against it | Approve at the requested ceiling |
Guarded | Soft signals: probationary history, runtime assurance between the step-up floor and the approve target | Approve with a reduced ceiling |
Elevated | Material signals: stale or thin receipt history, reputation below the approve threshold, missing runtime assurance | Reduce the ceiling or step up |
Critical | Hard failures: failed or revoked certification, failed settlement exposure, reputation below the deny threshold | Deny |
Reason codes
UnderwritingReasonCode labels each input UnderwritingSignal. Thirteen codes are defined, each with its trigger, the severity it carries, and its typical routing.
| Code | Trigger | Severity | Typical routing |
|---|---|---|---|
Probationary | Subject is inside its reputation tier's probationary window | Guarded | ReduceCeiling |
LowReputation | Effective score below the approve threshold, or below the deny threshold | Elevated or Critical | ReduceCeiling or Deny |
Imported | Reputation depends on signals imported from another peer | Guarded | ReduceCeiling |
Missing | The tool requires a certification record and none was found | Elevated | StepUp, when policy requires certification |
Failed | A certification check returned a failing verdict | Critical | Deny |
Revoked | A previously valid certification was revoked | Critical | Deny |
Missing | No runtime attestation observed for governed receipts | Elevated | StepUp |
Weak | Tier below the approve target and above the step-up floor | Guarded | ReduceCeiling |
Pending | Outstanding receipts are awaiting settlement | Guarded or Elevated | ReduceCeiling |
Failed | One or more prior settlements failed | Critical | Deny |
Metered | Metered usage and billed amount disagree | Guarded or Elevated | ReduceCeiling |
Delegated | The call came through a delegation chain that adds risk | Guarded | ReduceCeiling |
Shared | Evidence references shared sources that need additional proof | Guarded | ReduceCeiling |
These codes are the input vocabulary. The evaluator translates them into UnderwritingDecisionReasonCode values on findings, such as PolicySignal, ReputationBelowApproveThreshold, and RuntimeAssuranceBelowStepUpTier, and keeps the original signal reason on the finding.
Decision outcomes
UnderwritingDecisionOutcome has four ordered variants; the report outcome is the maximum over every finding.
| Outcome | Meaning | Underwriting | Underwriting |
|---|---|---|---|
Approve | Grant the requested ceiling at full strength | Approved | Preserve |
ReduceCeiling | Grant a narrower ceiling, multiplied by reduce_ | Approved | Reduce |
StepUp | Hold pending manual review or stronger evidence | Manual | Hold |
Deny | Reject; no ceiling is granted | Denied | Deny |
Decision policy and defaults
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,
}
}
}Lookback windows
Receipt freshness is governed by maximum_receipt_age_seconds on the policy, 30 days by default. Premium pricing in price_premium uses its own window, a LookbackWindow { since, until }, recorded on the resulting PremiumQuote for audit.
Evidence kinds
Findings reference UnderwritingEvidenceReference records, each tagged with an UnderwritingEvidenceKind that routes it back to the system that produced it. A RuntimeAssuranceEvidence reference includes a SHA-256 digest and a verifier locator pointing at the attestation behind the finding.
pub enum UnderwritingEvidenceKind {
Receipt,
ReputationInspection,
CertificationArtifact,
RuntimeAssuranceEvidence,
SettlementReconciliation,
MeteredBillingReconciliation,
SharedEvidenceReference,
}UnderwritingComplianceEvidence
An optional compliance score in the input bundle has this fixed shape, with score on the kernel compliance score's 0 to 1000 scale and generated_at in Unix seconds.
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 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 beside the receipt evidence: reputation, certification, runtime assurance, and the compliance score. Only the reputation block admits evidence that originated on another node, reporting arrivals in importedSignalCount and local acceptances in acceptedImportedSignalCount.
An accepted count above zero raises an ImportedTrustDependency signal in class Guarded, naming the count and referring back to its reputation inspection, so a subject whose standing leans on another node's signals gets a closer review. Portable Reputation covers how a signal crosses nodes.
Reputation evidence
The reputation block on UnderwritingPolicyInput has a fixed shape:
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,
}Decision-time premium quote
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,
}Insurance-flow premium
chio-market quotes and binds an insurance policy with the standalone price_premium function. It takes a compliance score in 0..=1000 plus an optional behavioral z-score and returns a PremiumQuote. It is deterministic and fails closed: a missing compliance score returns a decline.
//! `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: 50 points per sigma above a 3-sigma threshold, capped at 250 points. The quote's justification string records the inputs used.
The z-score comes from chio_kernel::behavioral_anomaly_score. The signed insurer-facing feed chio trust behavioral-feed export (HTTP GET /v1/reports/behavioral-feed) exports it as PremiumInputs.behavioral_z_score. Liability Market binds the quote with quote_and_bind.