Chio/Docs
LOGIN · JOIN

BuildEconomics

Settlement

Settlement transfers payment from the caller to the provider for a tool invocation, using payment adapters or on-chain escrow.


Prerequisites

  • A rail to settle over. Off-chain rails need a PaymentAdapter implementation; the Web3 rail needs a funded chain configuration and the deployed contracts.
  • A governed intent carrying a settlement_mode. A call with no settlement intent settles nothing, whatever adapter is wired.
  • A receipt store, because settlement state is read off signed receipts rather than out of adapter memory.
  • Every amount here is in minor currency units: 100 units is one US dollar (crates/core/chio-core-types/src/capability/scope.rs:80-91).
  • To reproduce this page's captures you need nothing but the CLI: chio mcp governed-sim builds an ephemeral kernel and a deterministic simulated adapter.

Settlement rails

A settlement rail is the mechanism used to move capital between counterparties. Each rail is represented by a CapitalExecutionRailKind variant in the chio kernel:

RailDescription
ManualHuman-initiated transfers outside the system (invoices, checks). Settlement is recorded manually.
ApiPayment processor integrations (Stripe, Adyen). Pre-authorization and capture via the PaymentAdapter trait.
AchACH bank transfers. Lower cost, higher latency. Suitable for batch settlement.
WireWire transfers for high-value settlements with same-day finality.
LedgerInternal ledger entries between accounts within the same operator. No external capital movement.
SandboxDevelopment rail that simulates settlement without moving funds.
Web3On-chain settlement via the chio-settle crate. Supports EVM (Base / Arbitrum) escrow, the bounded Solana Ed25519 memo path, and Chainlink CCIP cross-chain coordination.

The rail is selected per-invocation based on the operator's configuration, the counterparty's supported rails, and the settlement amount. Most operators start with Sandbox during development and graduate to Api or Web3 in production.


Payment Adapter Integration

For off-chain rails (Api, Ach, Wire), settlement flows through the PaymentAdapter trait. The trait is synchronous. Seven methods, four of which carry the payment lifecycle and three of which describe or query the rail:

crates/kernel/chio-kernel/src/payment.rs21-84rust
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(),
        ))
    }
}

Read the idempotency contracts, because they are the part an implementation gets wrong. authorize is idempotent by request.reference; capture and release are idempotent by the pair (authorization_id, reference). settlement_state has to stay answerable when authorization_id is absent, so recovery can close the window between authorizing and the rail identifier reaching the local journal.

The API uses request arguments instead of a Money type. The authorization object tracks the state of each payment flow, and capture, release, and refund all return one shared PaymentResult:

crates/kernel/chio-kernel/src/payment/types.rs8-15rust
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,
}
rust
pub struct PaymentResult {
    pub transaction_id: String,
    pub settlement_status: RailSettlementStatus,
    pub metadata: serde_json::Value,
}

There is no settled: bool. Whether the authorization created a reversible hold or completed a final prepayment is a two-variant enum, PaymentAuthorizationState with Held and PrepaidFinal, read as authorization.state.is_final()(crates/kernel/chio-kernel/src/payment/types.rs:17-29).

Pre-authorization flow

The standard pattern is: pre-authorize for max_cost_per_invocation before the tool call, then capture the actual cost after the call completes. If the actual cost is zero (for example, a cached result), release the authorization instead of capturing it.

Here is the authorize-capture-release cycle:

rust
// 1. Pre-authorize the maximum possible cost.
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. Execute the tool invocation.
let result = invoke_tool(&request)?;

// 3. Capture the actual cost, or release the whole hold if 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)?;
}

Run a prepaid call, and then break it

chio mcp governed-sim drives one governed MustPrepay call through an ephemeral kernel and writes the signed receipt bundle to --out. It is the smallest way to see the settlement fields on a receipt without standing up a rail.

budget-receipts · allowtranscript
$ 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"
}
exit 0
One governed prepaid call and its financial metadata. preauthorized_units and recorded_units both read 100, so the hold and the capture agree and nothing was released; payment_reference is the adapter authorization id that joins back to the rail.
sourcecrates/core/chio-core-types/src/receipt/economics.rs:77-106at fe56570

Now remove the rail. Re-run with --payment-adapter none. A MustPrepay intent with no adapter configured is unpayable, so the kernel refuses before the tool runs. Every error the CLI prints is a three-line envelope: the code and message, a context object, and a suggested fix.

budget-receipts · denytranscript
$ chio --session-db ./s2.sqlite mcp governed-sim \
    --governed-mustprepay --payment-adapter none --out ./bundle-deny.json
exit 1
A prepaid intent refused for want of an adapter, exit 1. The bundle is still written, so the refusal is durable rather than only returned.
sourcecrates/kernel/chio-kernel/src/kernel/validation.rs:2698-2713at fe56570

The nonzero exit is the visible half. The durable half is that the bundle carries a signed deny receipt:

budget-receipts · deny-receipttranscript
$ 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"
  }
}
exit 0deny
The deny receipt behind the refusal. attempted_cost records what was asked for, cost_charged is zero, the budget is untouched at its total, and the guard is the kernel rather than any named guard.
sourcecrates/kernel/chio-kernel/src/kernel/responses/deny_responses.rs:31-90at fe56570

Compare the two financial blocks. On the allow, cost_charged is 100 and budget_remaining drops to 900. On the deny, attempted_cost records what was asked for, cost_charged is 0, and the budget is untouched at 1000. A refused call costs the holder nothing and still leaves the attempt on the record with its price attached, which is what makes a denial auditable rather than merely absent.


On-Chain Settlement (Web3 Rail)

The Web3 rail settles tool invocation costs on-chain through smart contracts. A Merkle proof lets a provider release escrow without the operator or an intermediary.

rendering
Escrow lock, then settle through DualSignature or through MerkleProof. The MerkleProof path needs no counterparty signature.

Smart Contracts

The on-chain settlement system uses these contracts:

ContractRole
ChioEscrowHolds funds in escrow during tool invocations. Supports ERC-20 tokens with permit.
ChioBondVaultManages collateral bonds for credit facilities and autonomous agent execution.
ChioRootRegistryStores merkle roots published by the kernel for checkpoint anchoring.
ChioIdentityRegistryMaps did:chio identifiers to on-chain addresses for settlement routing.
ChioPriceResolverIntegrates oracle price feeds for cross-currency settlement and FX verification.

Settlement Paths

Once funds are locked in ChioEscrow, they can be released through two settlement paths:

  • DualSignature (two-party signed): the operator signs a release message authorizing the provider to withdraw. Settlement completes in one transaction after both parties sign the amount.
  • MerkleProof(inclusion-proof from the receipt log): the provider submits a merkle proof showing that the invocation receipt was included in a published checkpoint. The provider submits a valid proof against a root in ChioRootRegistry; the operator does not need to cooperate.

Choose the path at dispatch, not at release

Use DualSignature when both parties can sign promptly. Use MerkleProof for a dispute or an unresponsive operator. The contract permits either: EscrowTerms has no path field (contracts/src/interfaces/IChioEscrow.sol:7-16). The SDK does not. chio-settle refuses to prepare a release whose path does not match the one the dispatch declared (crates/economy/chio-settle/src/evm/prepare.rs:555-559,1129-1133), so the fallback has to be chosen before the escrow is funded.

Escrow Lifecycle

The lifecycle flows PendingDispatch -> EscrowLocked -> PartiallySettled / Settled / Reversed / ChargedBack / TimedOut, with Failed and Reorged for a settlement that did not land:

StateMeaning
PendingDispatchEscrow created but funds not yet locked on-chain
EscrowLockedFunds locked in the ChioEscrow contract, invocation can proceed
PartiallySettledSome funds released to the provider, remainder still held
SettledAll escrowed funds distributed
ReversedEscrow reversed, funds returned to the caller
ChargedBackDispute resolved in caller's favor after settlement
TimedOutEscrow expired without settlement, funds returned to caller
FailedThe settlement transaction failed
ReorgedA chain reorganization invalidated the settlement

ERC-20 token support uses the EIP-2612 permit pattern, allowing callers to approve and deposit in a single transaction without a separate approval step.

Payment-Rail Compatibility

Beyond escrow and bond settlement, the chio-settle crate exposes support for established on-chain payment methods, so a settlement can use an existing method instead of the native escrow contracts. Each integration provides preparation and verification functions in the crate's payments module:

  • x402 payment requirements (build_x402_payment_requirements, X402SettlementMode)
  • EIP-3009 transferWithAuthorization digests (prepare_transfer_with_authorization, Eip3009Domain, and an Eip3009NonceStore, which the prepare function takes as an argument rather than an option)
  • Circle nanopayments (evaluate_circle_nanopayment, CircleNanopaymentPolicy)
  • ERC-4337 paymaster-sponsorship checks (prepare_paymaster_compatibility, Erc4337PaymasterPolicy)

Setting Up an Escrow

Settlement uses Rust and SDK APIs

On-chain settlement is driven through the chio-settle crate's prepare / submit / confirm sequence. The economic layer exposes these as Rust and SDK APIs; there is no chio settle <chain> subcommand beyond chio settle status.

Create an escrow and settle it through DualSignature as follows. The SDK prepares each call, validates the capital instruction and identity binding, then submits and confirms it:

rust
use chio_settle::evm::{
    prepare_web3_escrow_dispatch, submit_call, confirm_transaction,
    finalize_escrow_dispatch, EscrowDispatchRequest,
};
use chio_core::web3::trust_profile::Web3SettlementPath;

// `config` (SettlementChainConfig), the signed capital instruction, and the
// operator's SignedWeb3IdentityBinding are assembled upstream.
let 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
//    identity binding and derives the escrow id via a static contract read.
let prepared = prepare_web3_escrow_dispatch(&config, &request, &operator_binding).await?;
// State: PendingDispatch -> (submit) -> EscrowLocked

// 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. Finalize the on-chain escrow id from the emitted event.
let dispatch = finalize_escrow_dispatch(&prepared, &tx_receipt)?.dispatch;

After the tool invocation produces a signed ChioReceipt, the operator prepares the dual-signature release and submits it. The release is configured to release the entire escrowed amount:

rust
use chio_settle::evm::{
    prepare_dual_sign_release, submit_call, confirm_transaction, DualSignReleaseInput,
};

let receipt = invoke_tool(&request).await?;

// 4. Prepare the dual-signature release for the observed amount.
// DualSignReleaseInput's key field is private and zeroizing, so it is built
// through its constructor rather than as a struct literal.
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 -> Settled

Settling via Merkle Proof

If the operator is unresponsive, the provider settles with an anchor inclusion proof once the receipt has been included in a published checkpoint. Choose the path at dispatch time, not at release time: the contract's EscrowTerms carries no path field, but chio-settle refuses to prepare a release that does not match the dispatch's declared path (crates/economy/chio-settle/src/evm/prepare.rs:555-559,1129-1133). So the dispatch has to have been created with Web3SettlementPath::MerkleProof:

rust
use chio_settle::evm::{
    prepare_merkle_release, submit_call, confirm_transaction, EscrowExecutionAmount,
};
use chio_core::web3::anchors::AnchorInclusionProof;

// 1. Build the anchor inclusion proof once the receipt's checkpoint root is
//    published to ChioRootRegistry (see 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 -> Settled

Checkpoint Anchoring

Checkpoints commit receipt history to on-chain registries. The chio-anchor crate publishes Merkle roots to ChioRootRegistry and verifies returned proofs against those roots.

Checkpoint anchoring can use three independent methods, modeled as AnchorLaneKind::{EvmPrimary, BitcoinOts, SolanaMemo} and verified together as an AnchorProofBundle:

  • EVM primary: merkle root published to ChioRootRegistry on an EVM chain (Ethereum L1 or L2)
  • Bitcoin OpenTimestamps: OpenTimestamps proof anchored to the Bitcoin blockchain for calendar-independent timestamping
  • Solana memo: checkpoint hash written as a Solana memo transaction for fast, low-cost redundancy

chio-anchor also owns a second, independent mechanism: 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) provides a fallback-verification path over a receipt batch.

A proof bundle links an individual receipt to a chain anchor:

bash
Receipt (content hash)
  -> Merkle inclusion proof (siblings + index)
    -> Checkpoint statement (root + timestamp + lane)
      -> Chain anchor (tx hash on EVM / BTC / Solana)

Verification is offline-capable

Once you have the proof bundle, verification requires only the chain anchor's transaction data and the merkle math. No kernel access, no API calls: just cryptographic verification against the published root.

Settlement Finality

Settlement finality depends on the chain where the escrow or checkpoint is anchored. Web3FinalityMode declares three modes and binds none of them to a chain (crates/economy/chio-web3/src/trust_profile.rs:26-32); the mapping is deployment configuration, carried per chain as Web3ChainFinalityRule with a chain_id, mode and min_confirmations(:55-59). The chains named below are the usual assignments, not values the enum carries:

Finality LevelChainMeaning
L1FinalizedEthereum mainnetTransaction included in a finalized epoch (~12 minutes)
OptimisticL2Optimism, Arbitrum, BaseSoft-confirmed on L2, subject to challenge window for full finality
SolanaConfirmedSolanaConfirmed by supermajority of validators (~400ms)

A dispute window is not an on-chain release gate. Both release paths in ChioEscrow execute immediately: releaseWithSignature settles the moment a valid dual signature lands, and releaseWithProofDetailed settles the moment a valid inclusion proof lands. Neither path carries a timelock; the only gate on either is the expiry check in _ensureLive(contracts/src/ChioEscrow.sol:352-355). Note the name: the five-argument releaseWithProof is a stub that reverts with ProofMetadataRequired(ChioEscrow.sol:158-166); the function that settles takes a ChioMerkle.Proof(:168-197), and partialReleaseWithProof splits the same way at :230. Dispute windows live one layer up, in two places, and neither is keyed on the settlement path.

The chio-settle observer selects a finality dispute window from the settlement amount. The default SettlementPolicyConfig tiers a dispute_window_secs across four bands of minor units:

Settlement amount (minor units)dispute_window_secs
up to 1,0000 (immediate)
up to 100,0003,600 (1 hour)
up to 1,000,00014,400 (4 hours)
above 1,000,00086,400 (24 hours)

inspect_finality_for_receipt applies this tiering identically whether the release used DualSignature or MerkleProof. It classifies a SettlementFinalityStatus (AwaitingConfirmations, AwaitingDisputeWindow, Finalized, Reorged) after the release; it does not block 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 for any path, including DualSignature, so a compliant profile cannot declare a zero-duration window for the fast path.

Web3SettlementLifecycleState defines states for successful and exceptional flows. The type is in chio-web3, re-exported via chio_core::web3:

crates/economy/chio-web3/src/settlement.rs39-49rust
pub enum Web3SettlementLifecycleState {
    PendingDispatch,
    EscrowLocked,
    PartiallySettled,
    Settled,
    Reversed,
    ChargedBack,
    TimedOut,
    Failed,
    Reorged,
}

Four of the nine are exceptional: Reversed returns funds before settlement, ChargedBack resolves a dispute after it, TimedOut is an escrow that expired without action, and Reorged is a chain reorganization invalidating one.

Reorg handling

The Reorged state is rare but important. Reorg handling is bounded, reviewable automation, not unconditional silent resubmission. The chio-settle observer classifies a reorg as SettlementFinalityStatus::Reorged and recommends SettlementRecoveryAction::ResubmitAfterReorg. Execution runs through a cron/log-triggered SettlementWatchdogJob that carries an operator_override_required flag; automation outcomes explicitly include ManualOverrideRequired.

Oracle Price Verification

When tool costs are denominated in one currency but settlement occurs in another (for example, costs in USD, settlement in USDC or ETH), the chio-link crate resolves verified exchange rates. Its ChioLinkOracle implements the PriceOracle trait and reads prices from a pluggable OracleBackend that supports both Chainlink (AggregatorV3Interface.latestRoundData) and Pyth (Hermes). The on-chain ChioPriceResolver contract is a separate, contract-level price read used on L2; the receipt-side oracle evidence is produced by chio-link.

The oracle applies several safety mechanisms:

  • Staleness checks: prices older than a configured threshold are rejected; the resolver denies conversion when the feed is stale.
  • L2 sequencer protection: on L2 chains (Optimism, Arbitrum, Base), it checks 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 backend read is cross-checked against a fallback with a basis-point circuit breaker, failing closed on divergence. Per-pair twap_enabled policy averages a rolling observation window into a TWAP.
  • Oracle evidence: cross-currency conversions produce an OracleConversionEvidence record that is stored on the receipt for auditability.
rust
// ChioLinkOracle implements PriceOracle: it resolves the rate from Chainlink
// and/or Pyth behind a divergence circuit breaker and an L2 sequencer check.
let rate = oracle.get_rate(&pair)?;

// The resolved rate converts into OracleConversionEvidence for the receipt.
let evidence = rate.to_conversion_evidence(/* conversion context */);
// evidence fields: schema, base, quote, authority, rate_numerator,
// rate_denominator, source, feed_address, updated_at, max_age_seconds,
// cache_age_seconds, converted_cost_units, original_cost_units,
// original_currency, grant_currency, oracle_public_key, signature.

Verify the result

Settlement is verified from the receipt, not from the rail. Three fields answer the question:

  • metadata.financial.settlement_status is settled on a completed prepaid call, not_applicable on a refused one, and failed on an overrun.
  • metadata.financial.payment_reference is the adapter's authorization id, which is the join key back to the rail's own record.
  • cost_breakdown.payment.preauthorized_units against recorded_units says whether the hold and the capture agreed. A gap between them is a release.

The captured allow under Run a prepaid call shows all three. For on-chain settlement, chio settle status reads the local lifecycle records.


Failures and recovery

No adapter for a prepaid intent

A MustPrepay intent with no adapter configured is refused before the tool runs, and the bundle is still written. That is the captured deny above. Recovery is configuration: wire an adapter, or change the intent's settlement mode. Do not retry the same call against the same absent rail.

A crash between authorize and capture

This is the window the trait's idempotency contracts exist for. authorize keyed on request.reference means a replayed authorization returns the same hold rather than creating a second one, and settlement_state stays answerable without an authorization_id so recovery can ask the rail what happened before the identifier reached the local journal. An adapter that skips either contract turns a crash into a double charge.

A reorg invalidated a settled transaction

The observer classifies it as SettlementFinalityStatus::Reorged and recommends SettlementRecoveryAction::ResubmitAfterReorg. Resubmission is not automatic: the watchdog job carries operator_override_required and stops at SettlementAutomationOutcome::ManualOverrideRequired rather than resubmitting silently.

The release path does not match the dispatch

Preparing a Merkle release against a dispatch declared as DualSignature, or the reverse, is refused by chio-settle before anything reaches the chain. There is no recovery at release time; the dispatch has to be created with the path you intend to fall back to.


Summary

ConceptDescription
Settlement RailsSeven rails (Manual, Api, Ach, Wire, Ledger, Sandbox, Web3) as CapitalExecutionRailKind variants
PaymentAdapterauthorize, capture, release, refund lifecycle for off-chain rails
On-Chain EscrowChioEscrow contract with DualSignature and MerkleProof settlement paths; the second needs no counterparty signature
Checkpoint Anchoringchio-anchor multi-lane proofs (EVM primary + Bitcoin OTS + Solana memo) plus the chio.anchor_batch.v1 witness mechanism
FinalityL1Finalized, OptimisticL2, SolanaConfirmed with chain-specific rules
Price Oraclechio-link (ChioLinkOracle) with Chainlink + Pyth backends, divergence circuit breaker, staleness and L2 sequencer checks

Next Steps