Chio/Docs
LOGIN · JOIN

Underwriting

Size an agent's spending ceiling from its record, then read the decision's reasons, risk class, and premium.

Concept chio@5f63e6e

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.

Underwriting decisionChoose an outcome.
ρreceipt historysreputationeffectiveScorevruntime assuranceπevaluationκApproveApproved · PreserveκReduceCeilingApproved · Reduce × 0.5τStepUpManualReviewRequired · HoldDenyDenied · DenyρreceiptssreputationeffectiveScorevassuranceπevaluationκApproveApproved · PreserveκReduceCeilingApproved · Reduce × 0.5τStepUpManualReviewRequiredDenyDenied · Deny

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.

Evidence evaluated under the default 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:

  • Approve keeps the requested ceiling at full strength: Approved, Preserve.
  • ReduceCeiling multiplies the ceiling by reduce_ceiling_factor: Approved, Reduce.
  • StepUp holds the request for manual review or stronger evidence: ManualReviewRequired, Hold.
  • Deny grants 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:

reputation-evidence.jsonjson
{
  "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.

policy-input.jsonjson
{
  "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.6 approves, subject to the other findings.
  • 0.25 <= score < 0.6 raises a ReputationBelowApproveThreshold finding and reduces the ceiling, or steps up when other elevated signals join it.
  • score < 0.25 raises a ReputationBelowDenyThreshold finding 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.

The premium follows the outcome and the risk class

Each signed UnderwritingDecisionArtifact carries an UnderwritingPremiumQuote in one of three states: Quoted, Withheld, or NotApplicable. Quoted basis points (100 bps is 1%) come from a fixed schedule keyed by outcome and rolled-up risk class.

OutcomeBaselineGuardedElevatedCritical
Approve100 bps150 bps200 bps300 bps
ReduceCeiling150 bps250 bps400 bps600 bps
StepUpWithheldWithheldWithheldWithheld
DenyNotApplicableNotApplicableNotApplicableNotApplicable

The quoted amount is ceil(exposure_units * bps / 10_000) in the currency of the quoted exposure; with no exposure supplied, the quote carries basis points only. A StepUp quote stays withheld until manual review or stronger evidence completes. Had the step-up trace above landed at ReduceCeiling at Elevated, the schedule would price it at 400 bps through this 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(),
    })
}

That quote would carry basis_points = 400 and quoted_amount = MonetaryAmount { units: 40, currency: "USD" }. The helper returns a Result: a multiplication that overflows u128, or a product past u64 minor units, is an error, never a truncated or wrapped premium, and the test quote_premium_amount_rejects_when_basis_points_force_overflow pins it. The separate insurance-flow function, price_premium, prices coverage that Liability Market binds.

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.

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

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:

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 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

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.

ClassReadingTypical 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.

CodeTriggerSeverityTypical routing
ProbationaryHistory
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
ImportedTrustDependency
Reputation depends on signals imported from another peer
Guarded
ReduceCeiling
MissingCertification
The tool requires a certification record and none was found
Elevated
StepUp, when policy requires certification
FailedCertification
A certification check returned a failing verdict
Critical
Deny
RevokedCertification
A previously valid certification was revoked
Critical
Deny
MissingRuntimeAssurance
No runtime attestation observed for governed receipts
Elevated
StepUp
WeakRuntimeAssurance
Tier below the approve target and above the step-up floor
Guarded
ReduceCeiling
PendingSettlementExposure
Outstanding receipts are awaiting settlement
Guarded or Elevated
ReduceCeiling
FailedSettlementExposure
One or more prior settlements failed
Critical
Deny
MeteredBillingMismatch
Metered usage and billed amount disagree
Guarded or Elevated
ReduceCeiling
DelegatedCallChain
The call came through a delegation chain that adds risk
Guarded
ReduceCeiling
SharedEvidenceProofRequired
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.

OutcomeMeaningUnderwritingReviewStateUnderwritingBudgetAction
Approve
Grant the requested ceiling at full strength
Approved
Preserve
ReduceCeiling
Grant a narrower ceiling, multiplied by reduce_ceiling_factor
Approved
Reduce
StepUp
Hold pending manual review or stronger evidence
ManualReviewRequired
Hold
Deny
Reject; no ceiling is granted
Denied
Deny

Decision policy and defaults

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,
        }
    }
}

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.

crates/economy/chio-underwriting/src/lib.rsrust
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.

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 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:

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,
}

Decision-time premium quote

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,
}

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.

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: 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.

Next: put a decision behind a credit line