Settlement Rails
Settle a priced call's charge after dispatch and reconcile the payment with the signed receipt that recorded it.
On this page
Settle what an agent spent: read the payment a priced call recorded on its receipt, plug a payment rail in behind the kernel, and release an on-chain escrow against the receipt itself.
The kernel holds the worst-case cost before dispatch; settlement happens after the call, against records the kernel signs. The steps start from the three 15-unit discount calls of Tool Pricing.
1. Read the payment on a priced call's receipt
Enter the run directory the discount service printed. Verify the first call's receipt against the public key the host selected before it requested any call.
Before you begin
Fetch the project with chio new pricing-metered --example pricing-metered, or download it, and run it as Budgets describes, with the price set to 15 units as in Tool Pricing. The host creates its stores and keys. Amounts on this page are in minor currency units.
cargo run --release --locked --bin verify-receipt -- \
call-1-receipt.json kernel-public-key.txt{
"authorized": true,
"crypto_error": null,
"crypto_floor": "allow_classical",
"decision": {
"verdict": "allow"
},
"ok": true,
"parameter_hash_valid": true,
"receipt_id": "607a245ba9f85470eb7277a30754527deeb47e8233880f583d0f8c53b50169ed",
"receipt_id_valid": true,
"signature_valid": true,
"signer_trusted": true
}You should see signer_trusted: true and signature_valid: true. The receipt's financial block carries the payment; your identifiers differ from these.
{
"budget_remaining": 35,
"budget_total": 50,
"cost_breakdown": {
"payment": {
"authorization_id": "hold:d6ffa273e4882e6a0454ef6f25bf01582dd488a368ca09015c9ea989e6b2ec1e",
"preauthorized_units": 15,
"rail": "finding-operator-ledger",
"rail_mode": "reversible_hold",
"recorded_units": 15,
"transaction_id": "capture:d6a78ca355ecb41e407513f49efe01fcea77bdefd253cfd0b511c16364584340"
}
},
"cost_charged": 15,
"currency": "USD",
"delegation_depth": 0,
"grant_index": 0,
"payment_reference": "capture:d6a78ca355ecb41e407513f49efe01fcea77bdefd253cfd0b511c16364584340",
"root_budget_holder": "4806f7a942076d27d3652d6d74b861455fce7eaeb7e235957ea26d8e87cb075d",
"settlement_status": "settled"
}| Field | Value | What it records |
|---|---|---|
cost_charged | 15 | A 15-unit charge in USD |
preauthorized_units, recorded_units | 15, 15 | The same fixed amount, authorized and captured |
authorization_id | hold:dd73…5387 | The hold, inside cost_breakdown.payment |
transaction_id, payment_reference | capture:7b42…1266 | The completed capture on this durable path |
rail, rail_mode | finding-operator-ledger, reversible_hold | Persistent local-credit accounting |
settlement_status | settled | The local adapter reported a completed capture |
The host checked each capture against the run's persisted payment records as well as its receipts, and each payment was captured once. The application holds a monetary capability with no governed intent, so this durable local-credit flow needs no separate governed settlement intent. Here settled means the local ledger captured the credit; a bank or chain transfer is a rail outcome, recorded the same way in step 6.
2. Plug a payment rail in behind the kernel
Payment rails plug in as optional modules behind one PaymentAdapter port. The kernel records the hold against the grant's budget and caps before dispatch; the adapter then authorizes on the rail, and captures or releases after the call.
Holds and caps prevent spend before dispatch
The kernel checks the grant and records the hold against its budget and caps, then asks the configured adapter to authorize up to the quoted amount. A payment on the rail never authorizes a call by itself: the grant and the hold do. PaymentAdapter lifecycle.
PaymentAdapter lifecycle. Holds and caps prevent spend before dispatch; what the rail does is observed and recorded. The modules are off in default builds; custody and the facilitator service are left out.Settlement railsHolds and caps stop the spend before dispatch, and a payment on a rail never authorizes a call by itself. The adapter authorizes up to the quoted amount, captures the observed cost, releases a zero-cost hold, and reverses later through refund. The host picks the adapter (sim, http-x402, http-acp) at configuration. The rail's RailSettlementStatus folds into the receipt's SettlementStatus (NotApplicable, Pending, Settled, Failed) beside the payment_reference, observed and recorded.
pub trait PaymentAdapter: Send + Sync {
fn rail_id(&self) -> &'static str {
"unspecified"
}
fn rail_mode(&self) -> Option<PaymentRailMode> {
None
}
/// Authorize or prepay up to `amount_units` before the tool executes.
///
/// Implementations must be idempotent by `request.reference`: repeating the
/// same request returns the same authorization and creates at most one
/// rail-side hold or prepayment.
fn authorize(
&self,
request: &PaymentAuthorizeRequest,
) -> Result<PaymentAuthorization, PaymentError>;
/// Finalize payment for the actual cost after tool execution.
///
/// Implementations must be idempotent by `(authorization_id, reference)`.
fn capture(
&self,
authorization_id: &str,
amount_units: u64,
currency: &str,
reference: &str,
) -> Result<PaymentResult, PaymentError>;
/// Release an unused authorization hold.
///
/// Implementations must be idempotent by `(authorization_id, reference)`.
fn release(
&self,
authorization_id: &str,
reference: &str,
) -> Result<PaymentResult, PaymentError>;
/// Refund a previously executed payment.
fn refund(
&self,
transaction_id: &str,
amount_units: u64,
currency: &str,
reference: &str,
) -> Result<PaymentResult, PaymentError>;
/// Return the side-effect-free rail state for a durable reference.
///
/// This query must remain answerable when `authorization_id` is absent so
/// recovery can close the crash window after authorization but before the
/// rail-assigned identifier reaches the local journal.
fn settlement_state(
&self,
reference: &str,
authorization_id: Option<&str>,
) -> Result<RailSettlementState, PaymentError> {
let _ = (reference, authorization_id);
Err(PaymentError::Unavailable(
"settlement_state query is unsupported by this payment adapter".to_owned(),
))
}
}Keep a stable operation reference across retries. authorize is idempotent by request.reference; capture and release are idempotent by the pair (authorization_id, reference). settlement_state answers without an authorization_id, so recovery can ask the rail what happened before the rail identifier reached the local journal.
// 1. Authorize the worst case before the tool runs.
let auth = adapter.authorize(&PaymentAuthorizeRequest {
amount_units: max_cost_units,
currency: "USD".to_string(),
payer: agent_id.clone(),
payee: tool_server_id.clone(),
reference: invocation_id.clone(),
governed: None,
commerce: None,
})?;
// 2. Run the tool invocation.
let result = invoke_tool(&request)?;
// 3. Capture the observed cost, or release the whole hold when nothing was spent.
let actual_units = result.metered_cost_units();
if actual_units == 0 {
adapter.release(&auth.authorization_id, &invocation_id)?;
} else {
adapter.capture(&auth.authorization_id, actual_units, "USD", &invocation_id)?;
}The downloadable host wraps these three calls in its durable payment journal. A governed intent that carries MustPrepay with no adapter configured is refused before the tool runs, with a signed deny receipt. Configure a compatible adapter to clear it, and change the settlement mode only when the application's payment contract changes.
Reproduce a prepaid call and its refusal with the simulator
A prepaid call, then the same call with no adapter
chio mcp governed-sim drives one governed MustPrepay call through an ephemeral kernel and writes the signed receipt bundle to --out. The simulator records 100 authorized and 100 charged; in this capture payment_reference carries the simulator's authorization ID.
$ chio --session-db ./s.sqlite mcp governed-sim \
--governed-mustprepay --payment-adapter sim --out ./bundle.json
$ jq '.metadata.financial' ./bundle.json{
"budget_remaining": 900,
"budget_total": 1000,
"cost_breakdown": {
"payment": {
"adapter_metadata": {
"adapter": "sim",
"commerce": null,
"governed": {
"approvalTokenId": "governed-sim-approval-1",
"intentHash": "f57e0f00b78fe5267f81f92696a0fe9a32339cb02d57b81c9e15f59996f3062d",
"intentId": "governed-sim-intent-1",
"purpose": "no-key CI smoke",
"serverId": "governed-sim-srv",
"toolName": "compute"
},
"mode": "prepaid_no_broadcast"
},
"authorization_id": "sim-268afdc9129342029708eb795c87da08",
"preauthorized_units": 100,
"recorded_units": 100
}
},
"cost_charged": 100,
"currency": "USD",
"delegation_depth": 0,
"grant_index": 0,
"payment_reference": "sim-268afdc9129342029708eb795c87da08",
"root_budget_holder": "0473a15a01ca05a11b0558cd9fd1edd8bddd26f3f598f1c168aaec97dd7f031b",
"settlement_status": "settled"
}Re-run with --payment-adapter none. The intent is unpayable, so the kernel refuses before the tool runs and the simulator exits 1 with the CLI's error code, context, and suggested fix.
$ chio --session-db ./s2.sqlite mcp governed-sim \
--governed-mustprepay --payment-adapter none --out ./bundle-deny.jsonThe bundle holds the signed deny: attempted_cost 100, cost_charged 0, the budget at its 1000 total, and the kernel as guard (the allow left 900). Both captures start from fresh allowances; Budgets covers a deny after prior spending.
$ jq '{decision, financial: .metadata.financial}' ./bundle-deny.json{
"decision": {
"verdict": "deny",
"reason": "governed transaction denied: governed intent mandates prepayment (settlement_mode=MustPrepay) but no payment adapter is configured",
"guard": "kernel"
},
"financial": {
"attempted_cost": 100,
"budget_remaining": 1000,
"budget_total": 1000,
"cost_charged": 0,
"currency": "USD",
"delegation_depth": 0,
"grant_index": 0,
"root_budget_holder": "95dd6964b2c519849a0f53ded5c810fc87791bb622ace9b887537de4c51a2c59",
"settlement_status": "not_applicable"
}
}3. Lock the payment in an EVM escrow
When two parties settle on chain, the depositor funds a ChioEscrow before the call. Opening it fixes the beneficiary, the token, the maximum amount, and the deadline; the receipt later authorizes the release.
Opening fixes both branches
Opening the escrow fixes the beneficiary, the token, the maximum amount, and the deadline. The depositor funds its own escrow, and those terms decide where a release and a refund can go. Predeclared settlement.
The operator's signature or an inclusion proof against ChioRootRegistry (step 4) authorizes the release. Either way the escrow checks the signer or proof, the beneficiary, the receipt, and an amount capped at the deposit before it transfers tokens; after the deadline the remainder refunds to the depositor.
Add the settlement crates, then prepare the dispatch with its release path. EscrowTerms on chain carries no path field and the contract exposes both entry points, so chio-settle records the path on the dispatch and refuses a release that differs from it.
cargo add --git https://github.com/backbay-labs/chio \
chio-settle chio-web3 chio-linkuse chio_settle::evm::{
prepare_web3_escrow_dispatch, submit_call, confirm_transaction,
finalize_escrow_dispatch, EscrowDispatchRequest,
};
use chio_web3::trust_profile::Web3SettlementPath;
// config (SettlementChainConfig), the signed capital instruction, and the
// operator's SignedWeb3IdentityBinding come from your settlement host.
let dispatch_request = EscrowDispatchRequest {
dispatch_id: dispatch_id.clone(),
issued_at,
trust_profile_id: profile_id.clone(),
contract_package_id: contract_package_id.clone(),
capability_id: capability_id.clone(),
depositor_address: caller_address.clone(),
beneficiary_address: provider_address.clone(),
capital_instruction: instruction.clone(),
settlement_path: Web3SettlementPath::DualSignature,
oracle_evidence_required_for_fx: false,
note: None,
};
// 1. Prepare the escrow-create call. This validates the instruction and the
// identity binding and derives the escrow id with a static contract read.
let prepared =
prepare_web3_escrow_dispatch(&config, &dispatch_request, &operator_binding).await?;
// State: PendingDispatch, then EscrowLocked once the create transaction confirms.
// 2. Submit and confirm the create transaction.
let tx_hash = submit_call(&config, &prepared.call).await?;
let tx_receipt = confirm_transaction(&config, &tx_hash).await?;
// 3. Read the on-chain escrow id from the emitted event.
let dispatch = finalize_escrow_dispatch(&prepared, &tx_receipt)?.dispatch;Your host supplies the SettlementChainConfig, the signed capital instruction, the operator identity binding, the contract package, and a funded depositor. Once the kernel signs the call's ChioReceipt, submit the signed release from the beneficiary account.
use chio_settle::evm::{
prepare_dual_sign_release, submit_call, confirm_transaction, DualSignReleaseInput,
};
// The governed tool call returns its signed ChioReceipt.
let receipt = invoke_tool(&tool_request).await?;
// 4. Prepare the dual-signature release for the observed amount.
// DualSignReleaseInput keeps its key field private and zeroizing,
// so it is built through its constructor.
let release = prepare_dual_sign_release(
&config,
&dispatch,
&receipt,
&DualSignReleaseInput::new(operator_settlement_key_hex.clone(), settlement_amount),
).await?;
// 5. Submit and confirm the release transaction.
let tx_hash = submit_call(&config, &release.call).await?;
confirm_transaction(&config, &tx_hash).await?;
// State: EscrowLocked, then Settled.You should see the lifecycle record move from PendingDispatch to EscrowLocked, then to Settled after the release, which must carry the permitted full-release amount from the beneficiary account. A cognition-market purchase between two organizations settles this way: its CrossOrgEscrow selector pins the settlement profile, and a funded escrow with a proof-gated release replaces the local hold and capture.
4. Anchor the checkpoint and release against its proof
chio-anchor publishes the receipt checkpoint's Merkle root to ChioRootRegistry. Once the receipt's checkpoint is published, the beneficiary releases with an inclusion proof and needs no new operator signature.
use chio_settle::evm::{
prepare_merkle_release, submit_call, confirm_transaction, EscrowExecutionAmount,
};
use chio_web3::anchors::AnchorInclusionProof;
// 1. Build the anchor inclusion proof once the receipt's checkpoint root is
// published to ChioRootRegistry by chio-anchor.
let anchor_proof: AnchorInclusionProof = build_anchor_inclusion_proof(/* ... */)?;
// 2. Prepare the Merkle release, binding the anchor proof to the dispatch.
let release = prepare_merkle_release(
&config,
&dispatch,
&anchor_proof,
&anchor_content, // SettlementAnchorContentBinding
EscrowExecutionAmount::Full,
)?;
// 3. Submit and confirm the release transaction.
let tx_hash = submit_call(&config, &release.call).await?;
confirm_transaction(&config, &tx_hash).await?;
// State: EscrowLocked, then Settled.This path needs a dispatch created with Web3SettlementPath::MerkleProof, a published root, and the operator's current identity binding. A Merkle release against a dispatch declared as DualSignature, or the reverse, is refused by chio-settle before anything reaches the chain; inspect the dispatch's declared path before you prepare a release. One proof bundle links the receipt to its chain anchor:
Receipt (content hash)
-> Merkle inclusion proof (siblings + index)
-> Checkpoint statement (root + timestamp + lane)
-> Chain anchor (tx hash on EVM / BTC / Solana)Signatures and inclusion check offline with their trusted inputs; whether an anchor is canonical and confirmed under your policy takes trusted chain evidence beyond a transaction hash.
5. Wait for finality before you book the transfer
An accepted release and a final settlement are separate observations. The release function checks beneficiary, active identity, signature or proof, amount, receipt reuse, pause state, and escrow liveness, and can transfer tokens before the observer classifies finality.
chio settle statuschio settle status reads the local lifecycle records; prepare, submit, and confirm run through the chio-settle crate. The observer's inspect_finality_for_receipt classifies each release as AwaitingConfirmations, AwaitingDisputeWindow, Finalized, or Reorged, from the configured confirmation count and a dispute window tiered by amount. Its Finalized is that policy assessment, separate from an independent consensus-finality proof.
A reorganization surfaces as SettlementFinalityStatus::Reorged with the recommendation SettlementRecoveryAction::ResubmitAfterReorg. The settlement watchdog runs a SettlementWatchdogJob on its cron or log trigger; a job that carries operator_override_required stops at ManualOverrideRequired until an operator acts.
6. Reconcile the receipt with the rail's record
A settlement is verified when the signed receipt, the rail's authoritative record, and your finality policy agree. Read three fields first:
metadata.financial.settlement_status:settledon a completed prepaid call,not_applicableon the refusal in step 2.metadata.financial.payment_reference: in the durable application, the capture transaction ID, whilecost_breakdown.payment.authorization_idseparately names the hold.cost_breakdown.payment.preauthorized_unitsagainstrecorded_units: the authorized and recorded amounts. A gap alone does not prove a release; inspect the terminal action and the rail's evidence.
The included collector verifies the same receipts and exports one accounting row per allowed charge.
cargo run --release --locked --bin export-billing -- . kernel-public-key.txtverified records: 3; non-allow records excluded from billing: 0 billing records: 3 total: 45 USD minor units
You should see three verified records totaling 45 USD minor units, each traceable to its signed receipt. For an external rail, match it against the rail's transaction record first. Export Billing Records carries the rows into an accounting system.
An interrupted payment keeps its hold until both sides resolve it
Delivery and money are separate facts, and each move changes only one of them. A crash between authorize and capture leaves the call's delivery known or unknown, and the money held.
An unknown outcome holds its money and is never re-dispatched
The reply is lost, so delivery is unknown and the money stays held until the outcome is resolved. The kernel refuses to capture against an unknown outcome and never dispatches the call again. Unknown outcomes.
A replayed authorization returns the same hold, because authorize is keyed on request.reference. A lost reply keeps its hold and is never dispatched again. After an unknown outcome, only a co-signed MutuallyAgreedUnknown releases the money; for a known return with a pending capture, only a ContractualCaptureWaiver stops the capture.
A hold blocks finalization, receipts, and delivery, but never a co-signed monetary successor; every finalizer reads the effective payment journal first, and a rotated signer signs with its current key bound to the original identity. Resolve the open operation before you submit new work against it.
Settlement types, contracts, and policies
Try Ledger, ChioEscrow, Reorged, or dispute_window_secs.
Settlement rail categories
A rail names the mechanism that settles a capital instruction. CapitalExecutionRailKind has seven categories; the host connects the executor for the category it uses and reconciles that executor's result.
| Rail | Settles through | Moves external funds |
|---|---|---|
Manual | A transfer the operator performs and evidences | Yes |
Api | An API-backed executor with its own credentials | Yes |
Ach | An ACH bank transfer | Yes |
Wire | A wire transfer | Yes |
Ledger | Entries between accounts of one operator | No |
Sandbox | A development simulation | No |
Web3 | On-chain settlement through chio-settle | Yes, for EVM escrow |
Timing and finality for Ach and Wire come from the configured rail and its evidence. Under Web3, EVM escrow, Solana memo evidence, and cross-chain preparation have different scopes: a memo or a prepared artifact is evidence, and only the escrow release moves tokens.
Adapters and payment results
The host selects an adapter at configuration: sim by default, http-x402 for an x402-style prepaid HTTP bridge, or http-acp for an ACP-style facilitator bridge, each talking to a compatible external service. The persistent local-credit ledger is a fourth, and SimPaymentAdapter is a deterministic verifier aid. Payment-rail and web3 modules are off in default builds of the kernel and CLI.
Authorization returns an identifier and a state. PaymentAuthorizationState separates a reversible Held authorization from PrepaidFinal; authorization.state.is_final() picks the terminal action. For a reversible-hold adapter, authorize max_cost_per_invocation before the call and capture the observed cost after it; release the hold when the cost is zero, as for a cached result.
pub struct PaymentAuthorization {
/// Payment rail's authorization or hold identifier.
pub authorization_id: String,
/// Whether authorization created a reversible hold or completed final prepayment.
pub state: PaymentAuthorizationState,
/// Rail-specific metadata such as idempotency keys, quote IDs, or expiry.
pub metadata: serde_json::Value,
}The terminal operations capture, release, and refund all return one PaymentResult:
pub struct PaymentResult {
pub transaction_id: String,
pub settlement_status: RailSettlementStatus,
pub metadata: serde_json::Value,
}Settlement contracts
| Contract | Holds | Read by |
|---|---|---|
ChioEscrow | Funds escrowed for a call, ERC-20 with permit | The beneficiary's release |
ChioBondVault | Collateral bonds | Credit facilities, autonomous execution |
Chio | Merkle roots the kernel publishes | Proof releases, anchor verification |
Chio | Operator identity and settlement keys | Escrow and root verification |
Chio | Oracle price reads on L2 | Cross-currency settlement, FX checks |
ERC-20 deposits use the EIP-2612 permit pattern, so a compatible token and a valid permit combine approval with the deposit. Token allowlisting, signatures, balances, and the contract's checks still apply. The five-argument proof entry point reverts; proof releases use the detailed entry point that carries the required metadata.
Release paths
- DualSignature. The operator signs the release digest and the beneficiary submits the transaction. The contract checks the active operator key and epoch, the bound receipt hash, the amount, and the beneficiary.
- MerkleProof. The provider submits a Merkle proof that the invocation receipt is in a published checkpoint, against a root in
ChioRootRegistry. No new operator signature is needed; the published root and the current identity binding are.
Escrow lifecycle states
The lifecycle type reports observed settlement states; each rail implements and checks its own transitions. Web3SettlementLifecycleState lives in chio-web3.
| State | Flow | Meaning |
|---|---|---|
PendingDispatch | Success | Dispatch prepared or submitted, before funds lock |
EscrowLocked | Success | Funds locked in ChioEscrow; the call can proceed |
Partially | Success | Some funds released to the provider, the rest held |
Settled | Success | All escrowed funds distributed |
Reversed | Exceptional | Funds returned to the caller before settlement |
ChargedBack | Exceptional | A dispute resolved for the caller after settlement |
TimedOut | Exceptional | A timeout or refund outcome the integration reports |
Failed | Exceptional | The settlement transaction failed |
Reorged | Exceptional | A chain reorganization invalidated the settlement |
Expiry alone executes no refund: TimedOut is the outcome the integration reports after it acts.
pub enum Web3SettlementLifecycleState {
PendingDispatch,
EscrowLocked,
PartiallySettled,
Settled,
Reversed,
ChargedBack,
TimedOut,
Failed,
Reorged,
}On-chain payment methods
Beside escrow and bond settlement, the payments module of chio-settle prepares and checks established on-chain payment methods, so a settlement can use one in place of the escrow contracts:
- x402 payment requirements:
build_x402_payment_requirements,X402SettlementMode. - EIP-3009
transferWithAuthorizationdigests:prepare_transfer_with_authorization,Eip3009Domain, and anEip3009NonceStorethe prepare function takes as an argument. - Circle nanopayments:
evaluate_circle_nanopayment,CircleNanopaymentPolicy. - ERC-4337 paymaster-sponsorship checks:
prepare_paymaster_compatibility,Erc4337PaymasterPolicy.
Anchoring lanes
Checkpoint anchoring uses three independent methods, AnchorLaneKind::{EvmPrimary, BitcoinOts, SolanaMemo}, verified together as an AnchorProofBundle:
- EVM primary. The Merkle root published to
ChioRootRegistryon an EVM chain, Ethereum L1 or an L2. - Bitcoin OpenTimestamps. An OpenTimestamps proof anchored to Bitcoin, for calendar-independent timestamping.
- Solana memo. The checkpoint hash written as a Solana memo transaction, for fast, low-cost redundancy.
chio-anchor also owns chio.anchor_batch.v1, which Merkle-batches checkpoint IDs and binds the batch to a public witness (Rekor or OpenTimestamps) through a WitnessPolicy. A bounded Chainlink Functions request, ChainlinkFunctionsTarget, gives a fallback verification path over a receipt batch.
Finality modes
Web3ChainFinalityRule associates a chain ID, a mode, and minimum confirmations in the trust profile. Configure the confirmations and waiting period per deployment; the mode name implies no elapsed time.
| Mode | Chain | Configure |
|---|---|---|
L1Finalized | Ethereum mainnet | The chain evidence L1 finality requires |
OptimisticL2 | Optimism, Arbitrum, Base | Confirmation and dispute policy |
SolanaConfirmed | Solana | The commitment policy for confirmation |
Dispute windows
The observer picks a dispute window from the settlement amount. The default SettlementPolicyConfig tiers dispute_window_secs across four bands of minor units, the same for DualSignature and MerkleProof releases:
| Settlement amount (minor units) | dispute_ | Duration |
|---|---|---|
up to 1,000 | 0 | Immediate |
up to 100,000 | 3,600 | 1 hour |
up to 1,000,000 | 14,400 | 4 hours |
above 1,000,000 | 86,400 | 24 hours |
The EVM observer queries the latest block and the receipt block's hash, then applies the confirmation count and the elapsed window; the classification never blocks the release call. Separately, a Web3TrustProfile declares a per-path dispute_windows vector: each entry pairs a settlement_path with challenge_window_secs, recovery_window_secs, and a dispute_policy (OffChainArbitration, TimeoutRefund, or BondSlash). validate_web3_trust_profile rejects a zero challenge or recovery window on every path, DualSignature included.
Currency conversion evidence
When a tool prices in one currency and settles in another (USD costs, USDC or ETH settlement), chio-link resolves the exchange rate. ChioLinkOracle implements PriceOracle over a pluggable OracleBackend that reads Chainlink (AggregatorV3Interface.latestRoundData) and Pyth (Hermes). The on-chain ChioPriceResolver is a separate contract-level price read on L2; receipt-side oracle evidence comes from chio-link.
- Staleness. A price past the configured age threshold is rejected, and the resolver refuses the conversion.
- L2 sequencer. On Optimism, Arbitrum, and Base the oracle reads the Chainlink sequencer uptime feed and fails closed while the sequencer is down or inside its post-recovery grace period.
- Cross-source divergence. The primary read is checked against a fallback with a basis-point circuit breaker that fails closed on divergence. Per-pair
twap_enabledpolicy averages a rolling window into a TWAP. - Evidence. Each conversion produces an
OracleConversionEvidencerecord stored on the receipt.
This helper takes a configured oracle and the converted amount the host calculated, resolves the pair, and validates the evidence's arithmetic. Feed configuration and the evidence signature follow the kernel's signing and trust policy.
use chio_web3::anchors::{
validate_oracle_conversion_evidence, OracleConversionEvidence,
};
use chio_link::PriceOracle;
pub async fn conversion_evidence(
oracle: &dyn PriceOracle,
original_units: u64,
original_currency: &str,
grant_currency: &str,
converted_units: u64,
now: u64,
) -> Result<OracleConversionEvidence, Box<dyn std::error::Error>> {
let rate = oracle.get_rate(original_currency, grant_currency).await?;
let evidence = rate.to_conversion_evidence(
original_units, original_currency, grant_currency, converted_units, now,
)?;
validate_oracle_conversion_evidence(&evidence)?;
Ok(evidence)
}The settlement CLI command is chio settle status; there is no per-chain chio settle subcommand.