Reconciliation
Follow one hold from its quoted ceiling to the charge, the returned headroom, and a settlement status an operator can act on.
On this page
An agent can be authorized for the worst case and charged for what it used. The kernel holds the quoted ceiling before dispatch, captures the observed cost after the tool returns, and gives the difference back to the grant.
Each reconciled receipt keeps the quote, the meter reading, and the rail's answer apart, so billing can explain an underspend and an operator can find a payment that is still open. The running example is an outside buyer's query against your orders server, quoted at 100 query units, one cent each, that finishes on a cheap indexed path.
A one-dollar hold closes at 85 cents and returns 15 to the grant
The kernel holds 100 cents against the grant, the query uses 85 units, and the adapter captures 85. The same reconcile step decides every other outcome a held call can have.
Capture the observed 85 and return 15 to the grant
The adapter captures 85 below the held 100, which unwinds the rest of the hold in the same operation. The budget hold closes at min(85, 100), and the receipt takes the rail result’s transaction_id as payment_reference. Settlement rails.
hold_capture, in USD minor units: what the kernel held, what the meter observed, and what the hold closes at in each case. Amounts are illustrative; a capture is drawn with a facilitator that answers settled, and later refunds are left out.The capture and the budget are separate facts. The adapter captures 85 cents on the rail, which unwinds the rest of the hold in one operation; the budget hold closes at actual_cost.min(charge.cost_charged), here min(85, 100), so the next call sees 15 cents of headroom. A zero observed cost calls release in place of capture, and an overspend never exceeds the authorized charge on the budget.
Three conditions close the hold at the full authorized charge. cross_currency_failed applies when the tool reports cost in another currency and no oracle rate resolves. payment_already_settled applies when the authorization was PrepaidFinal, which skips capture entirely. cost_overrun applies when the reported cost exceeds the authorized charge: the receipt records the reported amount while the budget keeps the authorized one. The currency failure and the overrun make no rail call and set settlement_status to Failed with the authorization id as payment_reference; a prepaid-final receipt settles from its authorization. A capture or release that errors records the same Failed status against the authorization id.
Follow the call through the ACP adapter
Under MustPrepay or HoldCapture, the kernel calls the adapter's authorize with the worst-case amount before dispatch and receives a PaymentAuthorization in state Held or PrepaidFinal.
The agent holds a capability bound to an AcpPaymentAdapter rail with settlement_mode = hold_capture. The adapter ships in chio-kernel, reports rail_id() as "acp" and rail_mode() as ReversibleHold, and confirms every monetary transition against an external facilitator. X402PaymentAdapter and SimPaymentAdapter return PrepaidFinal at step 2 and take the payment_already_settled branch. Any other rail plugs in by implementing the PaymentAdapter trait, as Settlement rails describes.
- Quote. The governed request binds a quote with
quotedCostof 100 cents andsettlementModeofhold_capture. - Authorize. The kernel builds a
PaymentAuthorizeRequestwhosereferenceis the durable operation id, or the request id when the call has no durable journal, and whoseamount_unitsis 100, then callsauthorize. The adapter posts it to the facilitator, checks that the response echoes the digest of the exact request, and returns aPaymentAuthorizationin stateHeldwith metadata{"adapter": "acp", "mode": "shared_payment_token_hold"}. The kernel refuses the authorization if that state disagrees with the rail mode already persisted in the payment journal. - Execute. The tool runs. The meter records
observed_unitsof 85 query units with a retained usage evidence id. - Capture. The kernel calls
capture(authorization_id, 85, "USD", request_id). The adapter binds the operation to the authorization id, the amount, the currency, and the reference, and the facilitator confirms it. Capturing below the held amount releases the unused 15 cents, so no second call follows. - Realize the budget. The hold closes at
min(85, 100)and 15 cents returns to the grant. - Sign the receipt.
ReceiptSettlement::from_payment_resultcopies the result'stransaction_idintopayment_referenceand maps its status throughto_receipt_status.
Quote, meter, and rail result keep separate records
A quote is an estimate, a meter is a measurement, and a rail result reports what the payment operation did. The receipt records each one where an auditor can trace it.
| Layer | Source | Final when | Carried on the receipt as |
|---|---|---|---|
| Quoted | MeteredBillingQuote.quoted_cost | At request binding | MeteredBillingReceiptMetadata.quote |
| Observed | Cost meter, after execution | When the tool returns or errors | MeteredUsageEvidenceReceiptMetadata.observed_units |
| Charged | Recorded amount and original rail result | When the payment outcome is confirmed | FinancialReceiptMetadata.payment_reference and settlement_status |
The quote and the usage evidence sit under the reserved governed_transaction key; the charge sits under financial. Read the recorded amount together with its settlement status and the original rail result, because the number alone proves no transfer. The worked call's receipt, the buyer's order-history query against your orders server under its USD 5.00 grant:
{
"governed_transaction": {
"intent_id": "intent-basalt-orders-001",
"intent_hash": "[64 hex, GovernedTransactionIntent::binding_hash]",
"purpose": "Basalt order-history lookup",
"server_id": "orders",
"tool_name": "sql",
"metered_billing": {
"settlementMode": "hold_capture",
"quote": {
"quoteId": "quote-orders-001",
"provider": "orders",
"billingUnit": "query-unit",
"quotedUnits": 100,
"quotedCost": { "units": 100, "currency": "USD" },
"issuedAt": 1747776000
},
"usageEvidence": {
"evidenceKind": "acp",
"evidenceId": "[facilitator usage record id]",
"observedUnits": 85
}
}
},
"financial": {
"grant_index": 0,
"cost_charged": 85,
"currency": "USD",
"budget_remaining": 415,
"budget_total": 500,
"delegation_depth": 0,
"root_budget_holder": "[issuer public key, hex]",
"payment_reference": "[facilitator transaction id]",
"settlement_status": "settled"
}
}Cost metering observes compute time, data bytes transferred, and any per-call upstream API cost. Every amount is a MonetaryAmount of exactly units and currency in minor units, so a sub-cent rate is expressed through the unit count: 1,000 rows priced at 100 cents.
Seven rail answers fold into three receipt statuses
The kernel records what the rail reports. to_receipt_status maps the seven values of RailSettlementStatus onto the receipt's SettlementStatus.
Three rail answers leave the receipt pending
Authorized, Captured, and Pending all fold to Pending. A facilitator that answers captured where settled was expected leaves the receipt pending and the exposure ledger entry asking for action. Settlement rails.
RailSettlementStatus on the left folds into the receipt’s SettlementStatus on the right through to_receipt_status. NotApplicable marks a receipt with no charge; the payment journal and the authorization record are left out.Authorized, Captured, and Pending become Pending; Settled, Released, and Refunded become Settled; Failed stays Failed. A facilitator that answers captured where settled was expected leaves the receipt pending and its ledger entry asking for action. Because three cash flows read as Settled, keep the original rail status beside the receipt before classifying the money.
A settled status reports the rail. Whether a receipt proves spend is decided by is_authoritative_spend_receipt: it requires a mediated allow, a budget hold whose terminal mutation was a reconcile, a signed execution nonce (chio.execution_nonce.v1) that binds the exact call, and an admitted kernel key. The frozen receipt profile it pins is chio.mediated_spend.v1, and BudgetGuaranteeLevel records how strong the store behind the hold was.
Read more in Authoritative spend, which holds the full conjunction and the guarantee levels, and in Budgets and metering, which teaches the lifecycle against a grant's budget.
An unfinished payment closes with new evidence beside the original receipt
Delivery and money are separate facts, and each moves on its own axis. After an unknown outcome, only a co-signed MutuallyAgreedUnknown releases held money; for a known return with a capture pending, only a ContractualCaptureWaiver stops the capture.
A known return captures on the money axis
The reply comes back, which is a delivery fact. Capture is a separate money fact: the hold moves to capture pending, then to captured at the observed cost. Each fact is recorded on its own axis. PaymentAdapter lifecycle.
A hold on the operation blocks finalization, receipts, and delivery until it clears, and the co-signed successor still passes. Every finalizer reads the effective payment journal first. A signer that has rotated signs with its current key and binds its original identity. Each open case below closes on the evidence named in it:
- Zero cost under a reversible hold. Release the authorization and inspect the bound response. The release result and the reconciled zero-cost budget record close the case; no beneficiary payment is implied.
- Capture pending. Keep the authorization and transaction references and follow the provider's observation path until a terminal rail result links to the original capture and receipt.
- Terminal request timed out. Resolve the original operation with the provider before retrying under its idempotency contract. A missing HTTP response is an unknown outcome, and the kernel never dispatches an unknown outcome again.
- A settled operation needs reversal. Use the adapter's
refundand confirm what it executed. For x402, a local terminal result is no proof of a remote refund. - Receipt marked
Failed. Investigate the loss record and choose a recovery action. New outcome evidence and an explicit reconciliation record close the case; the signed receipt stays as issued. - Loss marked
ReconciledorIgnored. Retain why attention was cleared and any supporting evidence. Either state clears the loss projection;Ignoredrecords no recovered money. - Observed cost contested. The dispute follows the claims path.
Settlements through the web3 module can stall on confirmations, a dispute window, or a chain reorganization. A watchdog job (chio.settlement-automation-job.v1) pins one dispatched settlement or bond by a state fingerprint and fires on a cron schedule. assess_watchdog_execution refuses an execution whose job, timing, fingerprint, or delay disagrees with the job, and every built job requires the operator's override. The operator's signer broadcasts whichever SettlementRecoveryAction the case needs.
Read more in On-chain settlement, which covers escrow states and finality.
Projections over receipts show open exposure and close the billing period
Across many settlements in flight, an operator needs one position per currency. chio trust exposure-ledger export projects the matching receipts into a signed chio.credit.exposure-ledger.v1 report. The positions below are the worked call plus one 60-cent capture still pending.
chio trust exposure-ledger export --capability cap-01a8…31f2 --json \
| jq '.body.positions'[
{
"currency": "USD",
"governedMaxExposureUnits": 200,
"reservedUnits": 100,
"settledUnits": 85,
"pendingUnits": 60,
"failedUnits": 0,
"provisionalLossUnits": 0,
"recoveredUnits": 0,
"quotedPremiumUnits": 0,
"activeQuotedPremiumUnits": 0
}
]Each receipt adds its governed max_amount to governedMaxExposureUnits and files its financial amount into exactly one of settled, pending, or failed by its SettlementStatus; a NotApplicable receipt adds nothing. A Pending or Failed receipt whose reconciliation state is still Open or RetryScheduled carries action_required, which counts its governed amount in reservedUnits, and a failed one also in provisionalLossUnits.
A failed settlement that never recovers passes through the credit loss lifecycle: bond impairment, then a facility drawdown for the residual, then a reconciliation state of Reconciled or Ignored that takes the amount off the next report. Recoveries reach recoveredUnits only through that lifecycle. The web3 settlement module is off by default; credit is part of every build.
The billing export closes the period. create_billing_export turns caller-selected receipts into a chio.billing-export.v1 export with one flat BillingRecord per receipt. The worked call becomes a record with cost_units of 85 and currency of "USD"; the 15 cents returned to the grant never appear.
- Live monitoring. Run the exposure ledger against an anchor and watch
pendingUnitsagainstgovernedMaxExposureUnits, andreservedUnitsas the backlog of receipts nobody has closed out. - Periodic close. Build the billing export at the billing cadence from the receipts that period covers.
- Watchdog review. Inspect watchdog executions whose outcome is other than
Executed; anything inManualOverrideRequiredis the action queue. - Loss documentation. When a failed settlement becomes a confirmed loss, drive the bond impairment if one applies, then move the receipt's reconciliation state off
Open. - Facility review. Cross-check reconciled volume against the configured facility ceiling and rebalance if utilization stays high.
A successful export covers the receipts you passed. State which unresolved or excluded payments remain outside it, and keep the receipt, the authorization and transaction references, the original rail status, the budget adjustment, and any recovery record together for review.
Read more in Export billing records, the operator workflow, and in Audit APIs, which shows how an auditor reads these receipts.
Reconciliation records and fields
Search for reservedUnits, replay_window_secs, or cost_units.
Receipt metadata keys
The metadata blocks under governed_transaction are camelCase on the wire while the block itself is snake_case: GovernedTransactionReceiptMetadata carries no rename_all, and MeteredBillingReceiptMetadata, MeteredBillingQuote, and MeteredUsageEvidenceReceiptMetadata all do. FinancialReceiptMetadata is snake_case throughout.
The rail side of the cycle is the PaymentAdapter trait in chio-kernel; the ledger side is chio-credit; the watchdog is chio-settle; the finance export is chio-metering.
Exposure ledger position
The report carries one position record per currency, serialized in camelCase.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ExposureLedgerCurrencyPosition {
pub currency: String,
pub governed_max_exposure_units: u64,
pub reserved_units: u64,
pub settled_units: u64,
pub pending_units: u64,
pub failed_units: u64,
pub provisional_loss_units: u64,
pub recovered_units: u64,
pub quoted_premium_units: u64,
pub active_quoted_premium_units: u64,
}quoted_premium_units and active_quoted_premium_units come from underwriting decisions. reserved_units and provisional_loss_units are gated on one boolean on each entry:
pub(crate) fn settlement_reconciliation_action_required(
settlement_status: SettlementStatus,
reconciliation_state: SettlementReconciliationState,
) -> bool {
matches!(
settlement_status,
SettlementStatus::Pending | SettlementStatus::Failed
) && !matches!(
reconciliation_state,
SettlementReconciliationState::Reconciled | SettlementReconciliationState::Ignored
)
}The flag fills reserve_required_amount on the entry, and a Failed receipt that still needs action also fills provisional_loss_amount. build_exposure_ledger_receipt_entry sets recovered_amount: None on every entry it builds; the position still sums recovered_units, which the credit loss lifecycle supplies.
Exposure ledger query
ExposureLedgerQuery refuses a query with no anchor. Its validate requires at least one of --capability, --agent-subject, or --tool-server, refuses a --tool-name without a --tool-server, and refuses a --since later than --until. Receipts default to 100 rows and cap at 200 (--receipt-limit); decisions default to 50 and cap at 200 (--decision-limit).
Credit loss lifecycle
- Provisional loss. The receipt settles
Failedwith its reconciliation state open, so its amount counts inprovisionalLossUnits. - Bond impairment. If a bond covers the loss,
prepare_bond_impairinchio-settlebuilds the prepared call against theChioBondVaultcontract, and the operator signs and broadcasts it. - Facility drawdown. If a credit facility covers the residual, the facility absorbs it and the operator documents the drawdown.
- Close. Marking the receipt's reconciliation state
ReconciledorIgnoredclearsaction_required, which removes the amount fromreservedUnitsandprovisionalLossUnitson the next report. The receipt keeps itsFailedstatus and the ledger stops asking for attention.
Settlement watchdog jobs
Each job fires on its cron expression and yields an execution record, which the watchdog validates against the declared job. Jobs carry the schema chio.settlement-automation-job.v1.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SettlementWatchdogKind {
EscrowTimeout,
FinalityObservation,
BondExpiry,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct SettlementWatchdogJob {
pub schema: String,
pub job_id: String,
pub kind: SettlementWatchdogKind,
pub trigger_kind: SettlementAutomationTriggerKind,
pub chain_id: String,
pub replay_window_secs: u64,
pub cron_expression: String,
pub state_fingerprint: String,
pub operator_override_required: bool,
pub reference_id: String,
}Two builders ship. build_settlement_watchdog_job takes a Web3SettlementDispatchArtifact and emits a FinalityObservation job whose state_fingerprint is a SHA-256 over the dispatch id, chain, escrow, token and beneficiary addresses, operator key hash, settlement path, and amount. build_bond_watchdog_job takes a chain, a vault id, and an expiry, and emits a BondExpiry job fingerprinted over those three. Both refuse an empty cron expression and a zero replay window, and both set operator_override_required: true and trigger_kind: Cron.
assess_watchdog_execution compares a job with an execution record and returns one of five refusals, each with its own message: the execution names a different job, it completed before it fired, its observed state fingerprint drifted, its delay exceeded the replay window without the outcome DelayedButSafe, or it suppressed a duplicate without the outcome DuplicateSuppressed. A sixth refusal covers the override:
if job.operator_override_required && !execution.operator_override_used {
return Err(SettlementError::Verification(
"watchdog execution must retain operator override control".to_string(),
));
}The seven SettlementRecoveryAction values are WaitForConfirmations, WaitForDisputeWindow, RetrySubmission, ResubmitAfterReorg, ExecuteRefund, ManualReview, and ExpireBond. The watchdog classifies the applicable action, and the operator's signer broadcasts any recovery transaction.
Billing record and export
Each export is a flat, denormalized record per receipt under chio.billing-export.v1.
pub struct BillingRecord {
/// Schema version.
pub schema: String,
/// Receipt ID.
pub receipt_id: String,
/// Unix timestamp (seconds) of the invocation.
pub timestamp: u64,
/// ISO 8601 timestamp for human-readable export.
pub timestamp_iso: String,
/// Session ID.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
/// Agent that triggered the cost.
pub agent_id: String,
/// Tool server.
pub tool_server: String,
/// Tool name.
pub tool_name: String,
/// Compute time in milliseconds.
pub compute_time_ms: u64,
/// Total data transferred in bytes.
pub data_bytes: u64,
/// Monetary cost amount (minor units).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost_units: Option<u64>,
/// Currency code for cost_units.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub currency: Option<String>,
/// Upstream provider that charged the cost.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
}pub struct BillingExport {
/// Schema version.
pub schema: String,
/// Export timestamp (Unix seconds).
pub exported_at: u64,
/// Total number of records in this export.
pub record_count: u64,
/// Total monetary cost across all records.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_cost: Option<MonetaryAmount>,
/// The billing records.
pub records: Vec<BillingRecord>,
}create_billing_export takes a slice of CostMetadata and an export timestamp. It sums total_cost only while every record it has seen shares one currency; the first record in a second currency sets a mixed flag and the export omits the total. provider is the first upstream API provider on the record's cost dimensions, documented in source with "openai" and "anthropic"; the payment rail is recorded elsewhere.