EconomyCredentials
Financial Credentials
The chio.fincred credential family, the passport that carries it, and the policy a receiver evaluates it against.
A financial credential is a signed Verifiable Credential that folds one part of an agent's economic record into a typed subject: credit standing, open exposure, settlement reliability, premium quotes, or losses. The crate chio-fincred (crates/economy/chio-fincred/src/lib.rs) defines the family: the schema ids, the five subject types, the source-evidence types, and the verifier policy. The crate chio-credentials (crates/trust/chio-credentials/src/financial.rs) issues, decodes, packages, and presents them. chio-credit projects an issuer's local signed reports into credential subjects.
The behavioral half of an agent's record travels separately, as reputation scorecards under chio.agent-passport.v1 (crates/trust/chio-credentials/src/lib.rs:40). Both halves obey the same import rule: a credential that crosses an issuer boundary carries presentationEvidenceClass = asserted, and the contract validator refuses any other value.
The chio.fincred family in the registry
The signed-artifact registry (spec/schemas/registry.json, bundle 1.0.0) lists these rows under the chio.fincred. prefix. Five are credential families; three carry the source evidence behind them.
| Schema id | What it carries |
|---|---|
chio.fincred.credit-scorecard.v1 | Band, confidence, overall score in [0, 1], probationary flag, and the imported-signal counts behind the score |
chio.fincred.exposure-history.v1 | Per-currency positions: governed max, reserved, settled, pending, failed, provisional loss, recovered |
chio.fincred.loss-history.v1 | Delinquency, recovery, reserve-release, reserve-slash, and write-off counts, with outstanding amounts |
chio.fincred.premium-history.v1 | Quoted premium count and the quoted amounts |
chio.fincred.settlement-reliability.v1 | On-time count, obligation count, and their ratio in basis points |
chio.fincred.source-checkpoint.v1 | A signed authority checkpoint: range root, index root, and the two window boundaries |
chio.fincred.source-completeness-attestation.v1 | The committed leaves and boundary proofs that fix which source records a window covers |
chio.fincred.source-member.v1 | One signed source artifact, keyed by family, subject, occurrence time, and artifact id |
Two further identifiers are source constants the registry does not list. chio.fincred.credential-id.v1 is the domain separator in the credential-id preimage, declared as FINANCIAL_CREDENTIAL_ID_DOMAIN in financial.rs. chio.fincred.verifier-policy.v1 is the schema tag a receiver pins on its own policy record, declared as FINANCIAL_VERIFIER_POLICY_SCHEMA_V1 in chio-fincred.
The five credential families
FinancialCredentialFamilyV1 names the five families, and each maps to one schema id, one Verifiable Credential type name, and one subject type. The subject is an adjacently tagged union: the wire form carries family as the tag and claims as the content, with snake_case family labels and camelCase claim fields.
pub enum FinancialCredentialFamilyV1 {
CreditScorecard,
ExposureHistory,
SettlementReliability,
PremiumHistory,
LossHistory,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(
tag = "family",
content = "claims",
rename_all = "snake_case",
deny_unknown_fields
)]
pub enum FinancialCredentialSubjectV1 {
CreditScorecard(CreditScorecardCredentialSubjectV1),
ExposureHistory(ExposureHistoryCredentialSubjectV1),
SettlementReliability(SettlementReliabilityCredentialSubjectV1),
PremiumHistory(PremiumHistoryCredentialSubjectV1),
LossHistory(LossHistoryCredentialSubjectV1),
}Monetary values are MonetaryAmount, and ratios are integer basis points. One field is floating point: overallScore on the credit-scorecard subject, which the validator requires to be finite and within [0, 1]. Integer fields pass an I-JSON safe-range check on the way in and out (ensure_i_json in crates/economy/chio-credit/src/financial_credentials/); a value above 253 minus one raises IJsonIntegerOutOfRange.
Each family maps to a signed issuer-local report that chio-credit projects from. Credit-scorecard folds a SignedCreditScorecardReport together with a SignedExposureLedgerReport; exposure-history folds the exposure ledger; premium-history folds SignedUnderwritingDecision records; loss-history folds SignedCreditLossLifecycle events. Each has one entry point named prepare_<family>_financial_source in crates/economy/chio-credit/src/financial_credentials.rs. Settlement-reliability has none.
The settlement-reliability ratio is derived, never copied from a caller. settlement_reliability_ratio_bps rejects a zero denominator, rejects an on-time count above the obligation count, and computes the ratio in u128 before narrowing:
pub fn settlement_reliability_ratio_bps(
on_time_count: u64,
obligation_count: u64,
) -> Result<u32, FinancialCredentialProjectionError> {
if obligation_count == 0 {
return Err(FinancialCredentialProjectionError::EmptyWindow);
}
if on_time_count > obligation_count {
return Err(FinancialCredentialProjectionError::InvalidReliabilityCounts);
}
ensure_i_json(on_time_count)?;
ensure_i_json(obligation_count)?;
let numerator = u128::from(on_time_count)
.checked_mul(10_000)
.ok_or(FinancialCredentialProjectionError::ReliabilityRatioOverflow)?;
u32::try_from(numerator / u128::from(obligation_count))
.map_err(|_| FinancialCredentialProjectionError::ReliabilityRatioOverflow)
}The credential envelope
FinancialCredentialEnvelope is the signed unit. It serializes camelCase, so a reader sees credentialId, issuerKeyEpoch, credentialSubject, and sourceEvidenceClass on the wire:
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FinancialCredentialEnvelope {
pub schema: String,
pub family: FinancialCredentialFamilyV1,
pub credential_id: String,
#[serde(rename = "@context")]
pub context: Vec<String>,
#[serde(rename = "type")]
pub credential_type: Vec<String>,
pub issuer: String,
pub issuer_key_epoch: u64,
pub issuance_date: String,
pub expiration_date: String,
pub credential_subject: FinancialCredentialSubjectV1,
pub evidence: FinancialCredentialEvidenceV1,
pub source_evidence_class: chio_core::capability::governance::ProvenanceEvidenceClass,
pub presentation_evidence_class: chio_core::capability::governance::ProvenanceEvidenceClass,
pub proof: CredentialProof,
}The validator fixes both JSON-LD arrays exactly. @context must equal ["https://www.w3.org/2018/credentials/v1", "https://chio.world/credentials/v1"], and type must equal ["VerifiableCredential", "Chio<Family>Credential"] for the family the schema names, for example ChioLossHistoryCredential. A mismatch between the schema id, the family tag, and the subject variant raises FinancialCredentialSchemaFamilyMismatch.
credential_id is derived rather than assigned. The issuer canonicalizes an id preimage covering the schema, family, context, type, issuer, issuer key epoch, issuance and expiration dates, subject, evidence, and both evidence classes, then takes SHA-256 over the domain separator chio.fincred.credential-id.v1\0 followed by those bytes. The id and the proof stay outside the preimage, so the id is stable across packaging and presentation, and a decoder recomputes it. The signing body is the same set of fields plus the derived id.
Source evidence and window completeness
An aggregate is meaningless without knowing which records it summed. Each credential carries FinancialCredentialEvidenceV1: the window, a source disclosure, and the signed completeness attestations behind it.
pub struct FinancialCredentialEvidenceV1 {
pub window: FinancialCredentialWindowV1,
pub source_disclosure: FinancialSourceDisclosureV1,
pub source_completeness_attestations: Vec<SignedFinancialSourceCompletenessAttestationV1>,
}The disclosure and the window boundary are both internally tagged enums, so the wire form carries a kind discriminant and refuses unknown fields:
#[serde(
tag = "kind",
rename_all = "snake_case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
pub enum FinancialSourceDisclosureV1 {
Bundled {
artifacts: Vec<FinancialSourceBundleArtifactV1>,
},
Resolver {
resolver_id: String,
references: Vec<FinancialSourceArtifactReferenceV1>,
},
}
// ...
#[serde(
tag = "kind",
rename_all = "snake_case",
rename_all_fields = "camelCase",
deny_unknown_fields
)]
pub enum FinancialSourceCompletenessBoundaryV1 {
SourceEdge,
Adjacent {
leaf_proof: FinancialSourceCommittedLeafProofV1,
},
}A disclosure either bundles the canonical source artifacts or names a resolver and references them by digest. Either way the attestation pins the checkpoint authority key and epoch, the store generation, the checkpoint sequence, a range_root and index_root, the committed leaves with their inclusion proofs, and the two window boundaries. A boundary is either SourceEdge, meaning the window runs to the end of the indexed range, or Adjacent, carrying the inclusion proof for the leaf immediately outside the window. That pair is what makes a count checkable: the receiver recomputes the range from the proofs instead of reading a number the issuer typed.
The passport carrier
The wire carrier is chio.financial-agent-passport.v1, which the registry files under kind agent_passport_v2. It sits beside the manifest, the presentation, and the challenge in spec/schemas/chio-trust/v1/:
| Schema id | What it carries |
|---|---|
chio.financial-agent-passport-presentation-challenge.v1 | A verifier's signed selector list, nonce, and challenge digest |
chio.financial-agent-passport.presentation.v1 | A disclosed subset with its membership proofs, challenge digest, and presentation digest |
chio.financial-agent-passport.source-manifest.v1 | The signed manifest over the packaged credentials: credential root, count, and validity |
chio.financial-agent-passport.v1 | The carrier: subject, credential list, Merkle roots, validity window |
A decoder dispatches on the top-level schema string before it deserializes a payload. chio.agent-passport.v1 decodes as the reputation-only passport; chio.financial-agent-passport.v1 decodes as the financial carrier and then runs its contract validator; any other value raises UnsupportedVersionedPassportSchema.
pub enum VersionedAgentPassport {
V1(AgentPassport),
V2(Box<AgentPassportV2>),
}The V2 carrier is a subject, a list of tagged credentials, and the Merkle roots that commit to them:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(
tag = "kind",
content = "credential",
rename_all = "snake_case",
deny_unknown_fields
)]
pub enum PassportCredentialV2 {
Reputation(Box<ReputationCredential>),
Financial(Box<FinancialCredentialEnvelope>),
}
// ...
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AgentPassportV2 {
pub schema: String,
pub subject: String,
pub credentials: Vec<PassportCredentialV2>,
pub merkle_roots: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub enterprise_identity_provenance: Vec<EnterpriseIdentityProvenance>,
pub issued_at: String,
pub valid_until: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trust_tier: Option<TrustTier>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_manifest: Option<SignedPassportSourceManifestV2>,
}Both credential variants are boxed, and the enum is adjacently tagged: the wire form is {"kind": "financial", "credential": { ... }}. Conversion between the two carriers is named and refuses to lose data. upgrade_v1_passport rewrites the schema tag and wraps each reputation credential as Reputation; try_downgrade_v2_passport returns PassportDowngradeWouldLoseData when the passport holds a financial credential or a source manifest.
The source manifest
Packaging a passport issues a signed PassportSourceManifestV2 over sorted leaves of (credential_id, family, credential_ref_digest). The manifest carries two roots, and both are checked when a presentation arrives.
pub struct PassportSourceManifestV2 {
pub schema: String,
pub source_passport_id: String,
pub issuer: String,
pub subject: String,
pub issued_at: u64,
pub expires_at: u64,
pub credential_count: u64,
pub credential_root: String,
pub merkle_roots: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub enterprise_identity_provenance: Vec<EnterpriseIdentityProvenance>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trust_tier: Option<TrustTier>,
}credential_root is the Merkle root over the manifest leaves. merkle_roots is the sorted set of source roots derived from the packaged credentials themselves, so a passport whose declared roots disagree with its credentials fails to build. issued_at and expires_at are Unix integers, and the expiry is clamped down to the earliest credential expiry in the bundle. source_passport_id is itself a digest over the rest of the body under the domain separator chio.financial-agent-passport.source-manifest-id.v1\0.
A leaf's credential_ref_digest is SHA-256 over the domain separator chio.financial-agent-passport.credential-ref.v1\0 and the canonical JSON of the family together with the complete signed credential. For a financial credential the leaf's credential_id is the credential's own derived id; for a reputation credential it is the reference digest.
Selective disclosure
Because each credential is signed on its own, a holder can drop the ones a verifier did not ask for without disturbing the remaining signatures. The disclosed form is its own type. It carries the exact signed manifest, the selected credentials, one membership proof per credential, and the two digests that bind the subset to one challenge.
pub struct PresentedAgentPassportV2 {
pub schema: String,
pub source_manifest: SignedPassportSourceManifestV2,
pub credentials: Vec<PassportCredentialV2>,
pub membership_proofs: Vec<PassportCredentialMembershipProofV2>,
pub challenge_digest: String,
pub presentation_digest: String,
}
pub struct PassportCredentialSelectorV2 {
pub family: PassportCredentialFamilyV2,
pub credential_ref_digest: String,
}
pub struct PassportPresentationChallengeV2 {
pub schema: String,
pub verifier: String,
pub verifier_key_epoch: u64,
pub challenge_id: String,
pub nonce: String,
pub issued_at: String,
pub expires_at: String,
pub source_passport_id: String,
pub selectors: Vec<PassportCredentialSelectorV2>,
pub challenge_digest: String,
}A challenge selects by exact envelope digest, not by issuer. One issuer can sign all five families, so issuer-level filtering would disclose more than the verifier asked for. Each PassportCredentialSelectorV2 names a family from PassportCredentialFamilyV2 (the five financial families plus Reputation) and the exact credential_ref_digest it will accept.
presentation_digest is SHA-256 over the domain separator chio.financial-agent-passport.presentation-digest.v1\0 and the canonical JSON of three values: the source-manifest digest (SHA-256 over the canonical signed manifest), the challenge digest, and one (credentialId, credentialRefDigest) entry per membership proof, in proof order. Disclosing a different subset changes that digest and leaves source_passport_id alone, so a presentation authorized for one subset does not replay as another subset of the same passport. Field-level unlinkable disclosure over individual claim fields has no type in the family; disclosure granularity is one credential.
Evidence classes
Provenance is graded on the ladder asserted < observed < verified, and a credential carries two positions on it. source_evidence_class is the issuer's grade for the local records it folded. presentation_evidence_class is the grade the receiving boundary may treat it as. Two checks fix both:
pub(super) fn validate_evidence_classes(
credential: &FinancialCredentialEnvelope,
) -> Result<(), CredentialError> {
if credential.presentation_evidence_class != ProvenanceEvidenceClass::Asserted {
return invalid_financial("financial presentation evidence class must be asserted");
}
let maximum = match &credential.credential_subject {
FinancialCredentialSubjectV1::CreditScorecard(subject)
if subject.imported_signals.accepted_imported_signal_count > 0 =>
{
ProvenanceEvidenceClass::Asserted
}
FinancialCredentialSubjectV1::SettlementReliability(_)
| FinancialCredentialSubjectV1::LossHistory(_) => ProvenanceEvidenceClass::Observed,
_ => ProvenanceEvidenceClass::Verified,
};
if evidence_class_rank(credential.source_evidence_class) > evidence_class_rank(maximum) {
return invalid_financial("financial source evidence class exceeds its family ceiling");
}
Ok(())
}Three consequences follow from those two checks. A credit-scorecard credential whose score accepted an imported signal cannot claim a source class above asserted, so imported trust does not launder itself upward one hop at a time. Settlement-reliability and loss-history cap at observed, because both depend on reconciliation state the issuer watches rather than proves. Exposure-history and a scorecard with no accepted imported signal may reach verified at their home issuer. Across the boundary all of them read as asserted: verifying a signature establishes who made a claim, and stops there.
Enterprise identity provenance
A passport can also record how an organization's identity provider verified the agent, in EnterpriseIdentityProvenance entries. The struct is defined in crates/trust/chio-credentials/src/artifact.rs and rides on both the carrier and its source manifest. Each entry names a provider_id, a provider_kind, the verified principal, the bound subject_key, and a federation_method drawn from EnterpriseFederationMethod: Jwt, Introspection, Scim, or Saml (crates/core/chio-core-types/src/session/auth.rs). Tenant, organization, client, object, groups, roles, per-attribute sources, and a trust-material reference are optional. A receiving organization reads that record beside the issuer signature, without reaching into the issuing organization's directory.
Verifier policy and the verified set
A receiver does not read credential subjects directly. It pins a policy record and evaluates the presentation against it.
pub struct FinancialVerifierThresholdsV1 {
pub min_credit_score: Option<f64>,
pub max_open_exposure_ratio_bps: Option<u32>,
pub min_settlement_reliability_bps: Option<u32>,
pub max_loss_event_count: Option<u64>,
pub max_premium_units_by_currency: BTreeMap<String, u64>,
}
pub struct FinancialVerifierPolicyV1 {
pub schema: String,
pub policy_id: String,
pub tenant: String,
pub verifier: String,
pub accepted_issuers: BTreeSet<String>,
pub accepted_families: BTreeSet<FinancialCredentialFamilyV1>,
pub thresholds: FinancialVerifierThresholdsV1,
pub max_credential_age_seconds: u64,
pub not_before: u64,
pub expires_at: u64,
pub configuration_generation: u64,
pub body_digest: String,
}evaluate_financial_credentials (crates/trust/chio-credentials/src/financial/authority/evaluation.rs) takes the presentation, the verified policy, a trust registry, a cross-issuer lifecycle resolver with its generation anchor and high-water store, an evidence source resolver, and a trusted clock. It returns one VerifiedFinancialCredentialSet: the source passport id, the manifest and presentation digests, the pinned policy id, body digest and generation, the lifecycle pins, and one VerifiedFinancialCredentialBindingV2 per accepted credential carrying its id, family, issuer, issuer key epoch, body and envelope digests, both evidence classes, and the source proof digests. A presentation that fails any check produces a CredentialError and no set, so there is no partly evaluated result to read.
Cross-issuer aggregation stays unmerged. A CrossIssuerPortfolio (schema chio.cross-issuer-portfolio.v1) lists independently verified passports, and a signed SignedCrossIssuerTrustPack (schema chio.cross-issuer-trust-pack.v1) activates issuers, profile families, entry kinds, migrations, and certification references that the local verifier already configured. No type in crates/trust/chio-credentials/src/cross_issuer.rs computes a combined score across issuers.
Limits
Trust is per issuer. No type in the family computes a merged financial score, and no issuer holds bureau-of-record standing: a receiver decides what each accepted credential is worth under its own pinned policy.
The family stops at the verified set. At the pin, evaluate_financial_credentials and VerifiedFinancialCredentialSet have no reader outside chio-credentials, so an operator that wants an accepted credential to reach an underwriting decision writes that step against the verified set itself. The underwriting signals the control plane derives come from local reputation, certification, and runtime-assurance evidence.
The settlement-reliability family does not decode
chio.fincred.settlement-reliability.v1 is registered and chio-fincred defines its subject type, but the family is refused on both paths. decode_financial_credential returns FinancialReliabilityProofSubstrateUnavailable from its preflight, before any field is read, and chio-credit's disclosure projection returns ReliabilityProofSubstrateUnavailable before it assembles one. The other four families issue, decode, and project.A bundle proves membership and a source class. It says nothing about records the issuer left out: the completeness attestation fixes the boundary of one window over one source, and a verifier that needs more than that window has to ask for another credential.
Related
- Agent Passports for the identity carrier, the
did:chiomethod, and the reputation credentials these sit beside. - Portable Reputation for the behavioral half of the record and the import rule it shares.
- Credit Scorecards for the local signed report an issuer folds into the credit-scorecard credential.
- Underwriting for the decision path a receiver runs after it accepts imported evidence.
- Schemas and Errors for the registry these rows come from.