Obligations
Track a receipt-backed debt apart from its current creditor, and sell that receivable once through an authenticated compare-and-swap.
On this page
A provider that finished paid work can sell what it is owed now instead of waiting for the due date, and any party can check who holds the right to collect at a given moment.
Each allowed, priced receipt finalizes into one immutable obligation. A separate disposition record names its creditor, and only the obligor authority’s compare-and-swap moves that record. The running example: a debtor owes a seller 10,000 units for completed work, and the seller sells the receivable to a buyer at 200 basis points.
The debt stays fixed while its creditor moves
Assignment changes who may collect the 10,000 units. The face value, the debtor, and the source receipt stay where the receipt put them.
The swap re-points the creditor and leaves the debt alone
In one transaction the obligor authority compares the expected disposition version, fences, and snapshot with the live records, moves per_call to assigned, and signs an acknowledgement. The atom, the debtor, and the 10,000 face value stay as they were. The compare-and-swap.
- Read eligibility. A fresh signed status proof shows a
per_calldisposition, a pending settlement, the seller as current creditor, the due date, and every validity window. That snapshot must still match at assignment. - Agree the sale. An offer and a signed agreement bind the claim, seller, buyer, discount, price, and expiry.
- Submit the assignment. The request binds the operation id and the expected current state. The store compares the live head atomically before it changes the creditor.
- Read the acknowledgement. The signed acknowledgement and its successor state name the buyer as creditor. The atom, the debtor, and the face value are unchanged.
- Pay and collect. The buyer pays the 9,800-unit purchase price; the debtor’s 10,000-unit debt settles to the acknowledged creditor. Retain the payment evidence for each.
At 200 basis points the price is floor(10,000 × 9,800 / 10,000) = 9,800 units. A successful acknowledgement names the creditor the debtor pays and records nothing about the purchase payment, which carries its own evidence.
One receipt yields one atom, and a separate record names its creditor
An ObligationAtomV1 binds the source receipt, the debtor, the receipt’s payee as original_creditor_id, the amount, and the due date. Its fields are private and ObligationAtomV1::new is its only constructor. Its obligation_id is a SHA-256 over the domain chio.obligation.id.v1, the economic intent digest, the source receipt digest, and a claim_index fixed at zero, so one receipt yields one claim and a duplicate atom fails validation.
The creditor lives beside the atom in one active chio.obligation.disposition.v1 record, carrying obligationId, atomDigest, the disposition, a version, a lifecycleFence, and the lastTransition that produced it. That record is the single serialization point for ownership. Its disposition is one of five kinds:
per_callpays the original creditor on the per-call path. It is the default and the only kind a first assignment starts from.assignedcarriesagreement_id,creditor_id, andsettlement_destination_ref. It is the only kind that moves the creditor off the original payee.channelizedcarrieschannel_idandreservation_idfor an obligation reserved to a payment channel.clearing_reservedcarries theround_idof a clearing round that has yet to settle.clearing_satisfiedcarries theround_idand thesatisfaction_digestof the round that settled it.
| From | Transition | To |
|---|---|---|
per_call | assign | assigned |
per_call | reserve_channel | channelized |
per_call | reserve_clearing | clearing_reserved |
clearing_reserved | release_clearing, same round | per_call |
clearing_reserved | satisfy_clearing, same round | clearing_satisfied |
Every other pair returns illegal obligation disposition transition. assigned has no outgoing arm. The assign arm also requires the incoming creditor_id to differ from both the debtor and the original creditor, and release_clearing carries an abort digest and a zero-dispatch proof digest.
A sale binds one claim, one offer, and one co-signed agreement
Selling a receivable at a discount is factoring: the seller is paid now and the buyer collects face value at settlement. Chio models the first assignment of one atom from one seller to one buyer. Bundled claims, fractional assignments, and secondary sales have no representation in the schema.
price.units = floor(face_value.units * (10_000 - discount_bps) / 10_000)Rates are integer basis points in 0..=10_000, computed over a checked u128 with no floating point. A discount above 10_000 is invalid factor field 'discount_bps'; an overflow, or a result beyond u64, is factor arithmetic overflow. Zero basis points prices at face value and 10_000 prices at zero. The price keeps the face value’s currency.
Three records carry the deal. The chio.factor.receivable-claim.v1 covers exactly one atom, one receipt, and one IOU, with a faceValue of at least one unit and a claimId derived from the rest of the body. The chio.factor.assignment-offer.v1 requires issued_at < expires_at < due_at, so an offer expires before the obligation falls due. The chio.factor.assignment-agreement.v1 binds both parties, the agreed discount and price, the buyer’s settlement destination, the operation id, and the versions and fences the parties expect. Signing it with two equal keys is refused.
Only the compare-and-swap transfers a receivable
Eligibility comes from a fresh chio.obligation.status-proof.v1, signed by the configured obligor authority from one transactional snapshot. The receipt’s own settlement status is receipt-time evidence. The proof shows that the obligation is per_call and pending now, with the seller as current creditor, inside the proof’s window and before the due date. The swap checks each of those conditions again at its effective time.
The public method that advances a disposition refuses any assignment with assignment requires an exact supplemental authorization compare-and-swap. The function that produces an assigned disposition is crate-private, and its one caller is the swap. In one transaction the store compares the proof with the live disposition version and fence, settlement-lifecycle version and fence, snapshot version, and resource fence, and any drift returns obligation compare-and-swap conflicted. A current factor.assignment_bind authorization, carrying an operation id the kernel derived, must name the same atom, seller, and buyer.
- Applied. The authority signs a
chio.factor.assignment-acknowledgement.v1, the authoritative transfer record, immutable once signed. ItspriorDispositionKindis fixed by the schema toper_call, so a document claiming a transfer out of any other state fails schema validation. - Not applied. The authority signs a
chio.factor.assignment-not-applied.v1with the expected and observed versions side by side and one of nine reasons:already_assigned,disposition_conflict,settlement_not_pending,status_proof_expired,authorization_expired,request_expired,offer_expired,past_due, oroperation_conflict.
Both results are stored against the operationId. A retry after a lost response reloads the stored record, checks that the commit replays it exactly, and returns it, so a timeout calls for the same operation again, never a new sale.
Of two concurrent agreements for one obligation, at most one is acknowledged; the other party reads the signed reason and refreshes the status proof before deciding again. Expired evidence needs a new agreement or proof, since changing a timestamp inside a signed record breaks its signature. The atom, status proof, signed agreement, assignment request, acknowledgement, and unchanged replay result together are the evidence of one sale.
Settlement pays whoever the disposition names
current_creditor returns the buyer’s creditor id and destination when the disposition is assigned, and the atom’s original creditor and destination otherwise. A clearing round calls the same function for each atom it resolves. reserve_channel and reserve_clearing both start from per_call, so an assigned obligation stays out of channels and rounds.
Settlement advances the settlement lifecycle beside the atom, from pending to settled with a settlement id and evidence digest, or to failed with a failure digest. The atom, the original receipt, the agreement, and the acknowledgement stay exactly as signed. Settlement Rails and Reconciliation cover how the payment is dispatched and closed.
An assignment is a direct bilateral first sale. The path holds no counterparty funds, issues no security, and runs no order book, matching engine, or automated venue. The receipt, the IOU, a seller-local log, and any seller-supplied exposure report are evidence; ownership transfers only with the obligor authority’s signed acknowledgement, so a signed agreement alone is an intention, and a transfer waits while that authority is unreachable. Factoring belongs to the optional credit module, off in default builds.
Obligation and factor records
Try status proof, not-applied, or claim_index. Each entry has its own link.
Obligation atom
#[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,
}validate recomputes obligation_id from the canonical encoding and rejects a mismatch, so obligationId on the wire is a 64-hex digest. payee_binding_digest must equal the digest derived from original_creditor_id and original_settlement_destination_ref together. Construction and every validate require a debtor different from the original creditor and a due_at_unix_ms 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 is in Credit Facilities.
Disposition wire shape
The disposition is an internally tagged union: an object with a kind discriminant beside the variant’s own snake_case fields, such as { "kind": "clearing_reserved", "round_id": "round-1" }, with no wrapper object keyed on the variant name.
Receivable claim
chio.factor.receivable-claim.v1 requires claimId, obligationId, obligationAtomDigest, sellerId, receiptId and receiptDigest, iouId and iouDigest, payeeBindingDigest, statusProofDigest, a faceValue whose units are at least one, dueAtUnixMs, and builtAtUnixMs. Like the atom id, claimId is derived from the rest of the body.
Assignment offer and agreement
chio.factor.assignment-offer.v1 binds the claim id and digest, the seller, an asking_discount_bps: u16 of at most 10_000, a minimum_price derived from the face value at that discount, and three times with issued_at < expires_at < due_at strictly.
chio.factor.assignment-agreement.v1 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. It travels as { body, sellerSignature, buyerSignature }, and signing refuses when the two signing keys are equal.
Discount quote
chio.factor.discount-quote.v1 binds the claim id and digest, an underwriting-decision digest, a scorecard digest, and an outcome that is a tagged union of two shapes: { "kind": "quoted", "resolved_discount_bps": …, "quoted_price": … } or { "kind": "refused", "refusal_reason": "…" }. Checking a quoted outcome against its claim recomputes the price from the face value and returns factor binding does not match on any difference. A face value of zero units, or a currency other than three uppercase letters, is rejected before the arithmetic runs.
Obligation status proof
chio.obligation.status-proof.v1 binds the obligation id and atom digest, the debtor, the original creditor, the resolved current creditor and settlement destination, the complete disposition 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. It is current while the trusted clock is at or after its issue time, strictly before its expiry, and strictly before the due date; outside that window it is obligation status proof is not current.
Verifying a receivable claim 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 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. An issuer key that is also a trusted kernel key is refused as a role confusion.
- The receipt is no later than the atom’s creation, which is no later than the proof’s issue time, which is no later than the claim’s build time. Any other order is
factor binding does not match.
Assignment bind authorization
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.
Assignment acknowledgement
chio.factor.assignment-acknowledgement.v1 travels as { body, signerKey, signature }. Later settlement state arrives as separate records. The body holds the full before and after of the move:
- 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(fixed toper_call),priorDispositionVersion,priorDispositionLifecycleFence,priorDispositionDigest,expectedSnapshotVersion,expectedResourceFence. - Swap.
resultingDispositionVersion,resultingDispositionLifecycleFence,resultingDispositionDigest,resultingSnapshotVersion,resultingResourceFence. - Time.
effectiveAtUnixMs,dueAtUnixMs,acknowledgedAtUnixMs.
Assignment not applied
chio.factor.assignment-not-applied.v1 carries every expected and observed version side by side, a no-mutation proof digest, and one of the nine reasons listed under the compare-and-swap. It is stored against its operationId like an acknowledgement.
Schema files
The obligation and factor families sit in the flat spec/schemas/chio-economy/ directory beside the channel and clearing files, so the base-name prefix separates them. The obligation- files describe what is owed and its current state; the factor- files describe one sale of it.
| File | Title | Registry id |
|---|---|---|
obligation- | Chio Obligation Atom V1 | chio. |
obligation- | Chio Obligation Disposition V1 | chio. |
obligation- | Chio Obligation Settlement Lifecycle V1 | chio. |
obligation- | Signed Chio Obligation Status Proof V1 | chio. |
factor- | Signed Chio Factor Assignment Acknowledgement V1 | chio. |
factor- | Signed Chio Factor Assignment Agreement V1 | chio. |
factor- | Signed Chio Factor Assignment Bind Authorization V1 | chio. |
factor- | Signed Chio Factor Assignment Not Applied V1 | chio. |
factor- | Chio Assignment Offer V1 | chio. |
factor- | Chio Discount Quote V1 | chio. |
factor- | Chio Normalized Assignment Request V1 | chio. |
factor- | Chio Receivable Claim V1 | chio. |
Wire conventions
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; payload fields inside a tagged union stay snake_case. Every digest is lowercase ^[0-9a-f]{64}$, and every time value is Unix milliseconds with a field name that says so.
Source
The model 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 each result for replay, in admission_operation_store/factor_assignment.rs.