EconomyDebts
Obligations
One immutable atom records what is owed, a separate authenticated record says who owns it, and one compare-and-swap moves that ownership once.
When a ChioReceipt finalizes as an allowed, priced call, the debt it created gets one record. That record never changes. Who currently owns the debt is a second record kept beside it, and moving ownership means changing that second record under a compare-and-swap, never editing the first. The whole model, from the atom through the assignment records, is pure code in chio-credit: obligation.rs and obligation/status.rs own the atom, its disposition, and the status proof, and factor/ owns the claim, offer, quote, agreement, and result validators. chio-store-sqlite holds the authenticated store that runs the swap and keys the result for replay, in admission_operation_store/factor_assignment.rs.
The obligation and factor schema files
Both families sit in the flat spec/schemas/chio-economy/ directory alongside the channel and clearing files, so the base-name prefix is what separates them. The registry lists an id for every one of the 12 files below, so none of these is a file whose id a reader has to infer. The 4 obligation- files describe what is owed and its current state; the 8 factor- files describe one sale of it.
| File | Title | Registry id |
|---|---|---|
obligation-atom.v1.json | Chio Obligation Atom V1 | chio.obligation.atom.v1 |
obligation-disposition.v1.json | Chio Obligation Disposition V1 | chio.obligation.disposition.v1 |
obligation-settlement-lifecycle.v1.json | Chio Obligation Settlement Lifecycle V1 | chio.obligation.settlement-lifecycle.v1 |
obligation-status-proof.v1.json | Signed Chio Obligation Status Proof V1 | chio.obligation.status-proof.v1 |
factor-assignment-acknowledgement.v1.json | Signed Chio Factor Assignment Acknowledgement V1 | chio.factor.assignment-acknowledgement.v1 |
factor-assignment-agreement.v1.json | Signed Chio Factor Assignment Agreement V1 | chio.factor.assignment-agreement.v1 |
factor-assignment-bind-authorization.v1.json | Signed Chio Factor Assignment Bind Authorization V1 | chio.factor.assignment-bind-authorization.v1 |
factor-assignment-not-applied.v1.json | Signed Chio Factor Assignment Not Applied V1 | chio.factor.assignment-not-applied.v1 |
factor-assignment-offer.v1.json | Chio Assignment Offer V1 | chio.factor.assignment-offer.v1 |
factor-discount-quote.v1.json | Chio Discount Quote V1 | chio.factor.discount-quote.v1 |
factor-normalized-assignment-request.v1.json | Chio Normalized Assignment Request V1 | chio.factor.normalized-assignment-request.v1 |
factor-receivable-claim.v1.json | Chio Receivable Claim V1 | chio.factor.receivable-claim.v1 |
Every body is RFC 8785 canonical JSON with deny_unknown_fields. Body fields are camelCase, so the Rust field due_at_unix_ms is dueAtUnixMs on the wire; the payload fields inside a tagged union are the exception and stay snake_case, as the disposition below shows. Every digest is lowercase ^[0-9a-f]{64}$, and every time value is Unix milliseconds with a field name that says so.
The obligation atom
One obligation per unit of value. ObligationAtomV1 binds the receipt it came from, the debtor, the receipt's payee as original_creditor_id, the amount, and the due date. Its fields are private and there is no setter: the only constructor is ObligationAtomV1::new, which validates and then hands back a value nothing can edit in place.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ObligationAtomV1 {
schema: String,
obligation_id: String,
claim_index: u32,
economic_intent_digest: String,
source_receipt_id: String,
source_receipt_digest: String,
debtor_id: String,
original_creditor_id: String,
original_settlement_destination_ref: String,
payee_binding_digest: String,
amount: MonetaryAmount,
credit_election: ObligationCreditElectionV1,
pre_action_authority_digest: String,
created_at_unix_ms: u64,
due_at_unix_ms: u64,
}Three of those fields do more work than their names suggest. obligation_id is not chosen by anyone: it is a SHA-256 over the canonical encoding of the domain string chio.obligation.id.v1, the economic intent digest, the source receipt digest, and the claim index, and validate recomputes it and rejects any mismatch. That is what makes a second atom for the same value impossible rather than merely discouraged, and it is why the wire type of obligationId is a 64-hex digest and not a label. claim_index is fixed at zero, so one receipt yields one claim. payee_binding_digest must equal the digest derived from original_creditor_id and original_settlement_destination_ref together, which pins the payee to the destination that pays it.
Two more checks run at construction and again on every validate: the debtor and the original creditor must differ, and due_at_unix_ms must be strictly after created_at_unix_ms. Either failure is invalid obligation field `obligation_terms`. credit_election records whether the debt was drawn against a facility, tagged not_credit or credit_facility with a facility id and authority digest; the facility accounting itself is in Credit Facilities.
One active disposition
The current creditor is not a field on the atom. It lives in a separate chio.obligation.disposition.v1 record carrying obligationId, atomDigest, the disposition itself, a version, a lifecycleFence, and the lastTransition that produced it. Exactly one such record is active beside a given atom, which is why a single serialization point governs who owns the receivable.
The disposition is an internally tagged union: on the wire it is an object with a kind discriminant beside the variant's own snake_case fields, so { "kind": "clearing_reserved", "round_id": "round-1" } rather than a wrapper object keyed on the variant name. There are five kinds:
kind | Carries | Meaning |
|---|---|---|
per_call | nothing | The default. Pays the original creditor on the per-call path, and the only kind a first assignment may start from. |
assigned | agreement_id, creditor_id, settlement_destination_ref | Sold. The only kind that moves the creditor off the original payee, and the one a first assignment produces. |
channelized | channel_id, reservation_id | Reserved to a payment channel. |
clearing_reserved | round_id | Reserved to a clearing round that has not settled yet. |
clearing_satisfied | round_id, satisfaction_digest | A clearing round settled it, with the satisfaction record named. |
Which moves are legal is a closed match on the pair of current disposition and proposed transition. Every pair outside the table returns illegal obligation disposition transition:
| From | Transition | To |
|---|---|---|
per_call | assign | assigned |
per_call | reserve_channel | channelized |
per_call | reserve_clearing | clearing_reserved |
clearing_reserved | release_clearing for the same round | per_call |
clearing_reserved | satisfy_clearing for the same round | clearing_satisfied |
Read what is missing from that table. assigned has no outgoing arm at all, so a resale has no representation: the schema cannot express a second sale, which is a stronger guarantee than a rule against one. The assign arm additionally requires the incoming creditor_id to be neither the debtor nor the original creditor, so a party cannot sell to itself or to the payer. And the two release arms are round-scoped: releasing back to per_call is legal only for the round that reserved the atom, and the release_clearing transition carries an abort digest and a zero-dispatch proof digest, so nothing quietly un-reserves.
Selling a receivable once
An agent owed money for a completed call can sell that receivable at a discount now instead of waiting for the due date; the buyer collects the face value at settlement. That is factoring, and this is the first-assignment case: one atom, one seller, one buyer, no resale. A bundled claim, a fractional assignment, and a secondary sale each have no representation in the schema.
The thing offered is a chio.factor.receivable-claim.v1 over exactly one atom, one receipt, and one IOU. It requires claimId, obligationId, obligationAtomDigest, sellerId, receiptId and receiptDigest, iouId and iouDigest, payeeBindingDigest, statusProofDigest, a faceValue whose units are at least one, and dueAtUnixMs and builtAtUnixMs. Like the atom id, claimId is derived from the rest of the body rather than supplied.
Two more records carry the bilateral deal. chio.factor.assignment-offer.v1 binds the claim id and digest, the seller, an asking_discount_bps: u16, a minimum_price derived from the face value at that discount, and three times. Validation requires the discount to be at most 10_000 and requires issued_at < expires_at < due_at strictly, so an offer can never outlive the obligation it sells. chio.factor.assignment-agreement.v1 then binds the claim, the offer, both parties, the agreed discount and price, the buyer's settlement destination, the normalized-request digest, the operation id, and the disposition and settlement-lifecycle versions and fences the parties expect to find. It travels as { body, sellerSignature, buyerSignature }, and signing refuses outright when the two signing keys are equal.
Pricing the discount
Every rate on this path is an integer basis-point value in 0..=10_000, with no floating-point arithmetic anywhere. The price is a floor over a checked u128 intermediate:
price.units = floor(face_value.units * (10_000 - discount_bps) / 10_000)Nothing here wraps or saturates. A discount above 10_000 is invalid factor field 'discount_bps'; a multiplication that overflows, or a result that will not fit a u64, is factor arithmetic overflow. A face value of zero units, or a currency that is not three uppercase letters, is rejected before the arithmetic runs. Zero basis points prices at full face value and 10_000 prices at zero, both legal. The resulting currency is the face value's own, so a quote cannot drift currency by construction.
The figure rides a chio.factor.discount-quote.v1, which binds the claim id and digest, an underwriting-decision digest, a scorecard digest, and an outcome. That outcome is a tagged union of exactly two shapes, not one struct with optional fields: { "kind": "quoted", "resolved_discount_bps": …, "quoted_price": … } or { "kind": "refused", "refusal_reason": "…" }. A quote is a price or a refusal and never both, and checking a quoted outcome against its claim recomputes the price from the face value and returns factor binding does not match on any difference.
Eligibility and the status proof
A receipt's settlement status is immutable receipt-time evidence. It does not prove the obligation is still unpaid at sale time, so it cannot be the eligibility check on its own. Eligibility instead requires a fresh chio.obligation.status-proof.v1 signed by the configured obligor authority and built from one transactional snapshot. Its body binds the obligation id and atom digest, the debtor, the original creditor, the resolved current creditor and settlement destination, the complete disposition value with its digest, version, and lifecycle fence, the settlement state with the same three, a snapshot version and resource fence, the due date, an issue and expiry time, and the authority id and key epoch.
A proof is current only while the trusted clock is at or after its issue time, strictly before its expiry, and strictly before the obligation's due date; outside that window it is obligation status proof is not current. Verifying a receivable claim then requires, at that one authoritative version:
- The disposition is
per_calland the settlement state ispending. - The proof's current creditor and destination equal the atom's original creditor and destination, and the atom's payee binding digest re-derives from that pair.
- The proof's disposition and settlement-lifecycle digests, versions, and fences all equal the records presented beside it, and its due date equals the atom's.
- The receipt and the IOU parse as canonical bytes and verify under trusted kernel keys and a retained IOU issuer trust, where an issuer key that is also a trusted kernel key is refused as a role confusion.
- The four timestamps run forward in causal order: the receipt is not later than the atom's creation, which is not later than the proof's issue time, which is not later than the claim's build time. Anything out of order is
factor binding does not match.
A capability decision authorizes the mutation itself. chio.factor.assignment-bind-authorization.v1 carries the literal action factor.assignment_bind and binds the atom digest, the seller, the buyer, the buyer's destination, the agreement id, the normalized-request digest, an action nonce, an issue and expiry time, and an operation_id the kernel derived under AdmissionOperationKind::GovernedEconomicMutation.
The compare-and-swap
The enforcement is structural rather than procedural. The public method that advances a disposition record refuses an assignment before it looks at anything else, with assignment requires an exact supplemental authorization compare-and-swap. The only function that can produce an assigned disposition is crate-private and reachable through exactly one caller: the compare-and-swap itself. There is no code path to a sold receivable that skips the swap.
Inside that one transaction the store compares the proof against the live records and refuses on any drift, returning obligation compare-and-swap conflicted. It requires the observed disposition to be per_call and not already assigned; the settlement state to be pending; the disposition version, disposition lifecycle fence, settlement-lifecycle version and fence, snapshot version, and resource fence all to equal the values the operation expected; the proof's current creditor to be the atom's original creditor; and the effective time to fall inside the proof's window and before the due date. The supplemental authorization must separately be current and must name the same atom digest, the same seller, and the same buyer. A seller-local log is audit evidence and can never stand in for any of this.
A successful swap emits a signed chio.factor.assignment-acknowledgement.v1. It is the authoritative transfer record, it is immutable once signed, and later settlement state arrives as separate records rather than as edits to it. Its body is the full before-and-after of the move:
| Field group | Members |
|---|---|
| Identity | acknowledgementId (derived from the rest of the body), operationId, normalizedRequestDigest, agreementId, agreementBodyDigest |
| Subject | obligationId, obligationAtomDigest |
| New owner | buyerId, buyerSettlementDestinationRef |
| Authority | assignmentAuthorizationSetDigest, statusProofDigest, authorityId, authorityKeyEpoch |
| Compare | priorDispositionKind, which the schema fixes to the single value per_call, plus priorDispositionVersion, priorDispositionLifecycleFence, priorDispositionDigest, expectedSnapshotVersion, expectedResourceFence |
| Swap | resultingDispositionVersion, resultingDispositionLifecycleFence, resultingDispositionDigest, resultingSnapshotVersion, resultingResourceFence |
| Time | effectiveAtUnixMs, dueAtUnixMs, acknowledgedAtUnixMs |
The whole record travels as { body, signerKey, signature }. That priorDispositionKind constant is the sharpest line on the page: an acknowledgement can only ever describe a move out of per_call, so a document claiming a transfer out of any other state fails schema validation before anyone has to reason about it.
A closed no-mutation outcome emits a signed chio.factor.assignment-not-applied.v1 instead, carrying every expected and observed version side by side, a no-mutation proof digest, and one of nine reasons: already_assigned, disposition_conflict, settlement_not_pending, status_proof_expired, authorization_expired, request_expired, offer_expired, past_due, or operation_conflict. A failed assignment is therefore a signed statement of why, not a silence. Either result is stored against its operationId: a retry after a lost response reloads the stored record, checks that the incoming commit replays it exactly, and returns the stored result rather than repeating the mutation. Of two concurrent agreements for one obligation, at most one can ever be acknowledged.
Settlement pays the acknowledged creditor
Who gets paid is one function on the disposition record, not a field read off the receipt. current_creditor returns the buyer's creditor id and destination when the disposition is assigned, and the atom's original creditor and destination in every other case. A clearing round calls exactly that function when it resolves each atom, which is how one answer stays authoritative across paths. The other two reservation paths cannot take an assigned obligation at all, because reserve_channel and reserve_clearing both start from per_call and nothing leaves assigned. Settlement advances the separate settlement lifecycle beside the atom, from pending to settled with a settlement id and evidence digest, or to failed with a failure digest. It never mutates the atom, the original receipt, the agreement, or the acknowledgement. See Settlement Rails and Reconciliation for how that movement is dispatched and closed.
The buyer pays the seller for the claim outside this path. The acknowledged re-point and the buyer-to-seller payment are two independently evidenced facts, and no record here asserts the second one happened.
What an acknowledgement is and is not
assigned disposition.See also
- Credit Facilities for the IOU, facility, bond, and exposure-ledger accounting the atom aggregates into.
- Clearing Rounds for multilateral netting and the
clearing_reserveddisposition a first assignment deliberately does not touch. - Reconciliation for how the record settlement emits closes the loop against the exposure ledger.
- Settlement Rails for the rails that move the face value to the acknowledged buyer destination.