Story 006 Settlement Rail Sprint 3¶
Based on:
- doc/project/30-stories/story-006.md
- doc/project/30-stories/story-006-buyer-node-components.md
- doc/project/20-memos/service-order-to-procurement-bridge.md
- doc/project/40-proposals/016-supervised-prepaid-gateway-and-escrow-mvp.md
- doc/project/50-requirements/requirements-007.md
- doc/project/50-requirements/requirements-008.md
- doc/project/60-solutions/node.md
Date: 2026-03-30
Status: Accepted hard-MVP planning note
Purpose¶
This memo freezes the hard-MVP settlement rail needed by story-006 after the
service-offer catalog and buyer-side service-order bridge.
The scope is deliberately narrow:
- one deployment-local supervised ORC ledger,
- one operator-driven top-up path,
- one hold state machine owned by the buyer-side Node host,
- and one append-only fact model that avoids rollback folklore.
It does not yet freeze a final remote buyer-to-escrow wire protocol.
Hard-MVP Decisions¶
1. Deployment-local settlement authority¶
For hard MVP:
- catalog and escrow ledger live inside the Node daemon,
- gateway may run as a separate local process on the same host when fiat ingress is needed,
- Node talks to gateway through an explicit local adapter,
- there is no requirement yet for a remote buyer-to-escrow protocol.
2. Lazy account creation¶
Ledger accounts are created lazily.
This means:
- reading a missing account returns zero balance,
- missing account during hold precheck is treated as insufficient funds,
- the buyer bridge maps that outcome to
settlement-blocked, - the ledger does not raise
AccountNotFoundfor the hard-MVP buyer path.
Frozen local account namespace for hard MVP:
account:participant:{participant_id}account:org:{org_id}account:community-pool
3. ORC arithmetic uses integer minor units only¶
ORC arithmetic MUST NOT use floating-point numbers.
Hard-MVP freeze:
ORCuses fixed decimal scale2,- ledger and bridge arithmetic use integer minor units only,
- display formatting into
major.minorlives in read or presentation layers, - no temporary conversion through
f64is admissible.
4. Hold creation is append-only, not transactional rollback¶
Hold creation and execution opening are modeled as append-only facts rather than a two-phase commit.
Bridge sequence:
- read-side balance precheck,
- append
HoldCreated, - open execution carrying
hold_id, - if execution opening fails, append
HoldVoided.
The hard-MVP deployment-local ledger may use a mutex around ledger mutation to reduce concurrent submission races, but the semantic model remains append-only.
5. Gateway receipt idempotency¶
The same gateway-receipt/id MUST NOT credit a local account more than once.
Hard-MVP write path:
- top-up ingestion deduplicates by
gateway-receipt/id, - the first application credits the account,
- later repeats return an idempotent already-applied outcome,
- no second credit transfer is recorded.
6. Hold state machine includes dispute freeze from the start¶
Hard-MVP hold states:
activefrozenreleasedrefundedvoided
Required transitions:
active -> releasedactive -> refundedactive -> frozenactive -> voidedfrozen -> releasedfrozen -> refunded
This keeps dispute handling explicit without refactoring the settlement rail later.
7. Settlement ledger is trait-owned¶
The daemon must depend on a thin behavior contract rather than a concrete inline implementation.
Recommended shape:
pub trait SettlementLedger: Send + Sync {
fn balance(&self, account: &AccountId) -> Result<OrcBalance, LedgerError>;
fn apply_top_up(
&self,
receipt: &GatewayReceiptRecord,
) -> Result<TopUpApplyOutcome, LedgerError>;
fn create_hold(&self, spec: HoldSpec) -> Result<HoldRecord, LedgerError>;
fn void_hold(&self, id: &HoldId, reason: &str) -> Result<HoldRecord, LedgerError>;
fn release_hold(&self, id: &HoldId) -> Result<HoldRecord, LedgerError>;
fn refund_hold(&self, id: &HoldId) -> Result<HoldRecord, LedgerError>;
fn freeze_hold(&self, id: &HoldId) -> Result<HoldRecord, LedgerError>;
fn hold_status(&self, id: &HoldId) -> Result<Option<HoldRecord>, LedgerError>;
}
Hard-MVP implementation:
LocalOrcLedger
Post-MVP freedom:
- remote escrow adapter,
- richer payout rail,
- alternative local storage implementation.
8. Top-up is operator-only in hard MVP¶
POST /v1/ledger/top-up is an operator surface guarded by the control-plane
auth token.
For hard MVP:
Arcadoes not invoke top-up directly,- top-up does not yet require a middleware-facing classified result,
- custodian-aware self-service top-up may be added later as a separate capability.
Data and Fact Model¶
The settlement rail should own append-only facts such as:
ledger/top-up-applied.v1ledger/hold-created.v1ledger/hold-voided.v1ledger/hold-frozen.v1ledger/hold-released.v1ledger/hold-refunded.v1
Read models are derived from those facts:
- account balance view,
- hold status view,
- top-up history,
- release or refund history.
Integration With The Buyer Bridge¶
The buyer bridge already owns:
- active offer resolution,
- order-to-offer sequence check,
- price and units computation,
- organization custodian resolution,
- settlement preflight gating,
- execution opening,
- buyer-local marketplace lineage.
Sprint 3 adds the settlement rail beneath that bridge:
- price computation yields one ORC minor-unit amount,
- settlement preflight reads lazy account balance,
- hold creation reserves value before execution open,
- execution state keeps
hold_id, - later procurement closure transitions the hold into release, refund, or freeze.
The bridge remains host-owned. Arca still does not author settlement facts.
Planned Implementation Order¶
A. Domain types and trait¶
Add one workspace crate for:
AccountIdHoldIdOrcAmount/OrcBalanceHoldSpecHoldRecordGatewayReceiptRecordLedgerFactLedgerErrorSettlementLedger
B. LocalOrcLedger¶
Implement:
- append-only facts,
- in-memory projector,
- restore-on-open semantics,
- lazy account balances.
C. Top-up write path¶
Add operator-only top-up ingestion with idempotent deduplication by
gateway-receipt/id.
D. Buyer-bridge hold integration¶
Add:
- balance precheck,
HoldCreated,- execution open carrying
hold_id, HoldVoidedon failed open.
E. Release and refund path¶
Tie terminal procurement outcomes to:
HoldReleasedHoldRefunded
F. Dispute freeze path¶
Tie dispute opening and dispute resolution to:
HoldFrozen- then
HoldReleasedorHoldRefunded
G. Control surface¶
Read-side minimum:
GET /v1/ledger/accountGET /v1/ledger/holds/{id}
H. Launcher wrapping¶
Thin launcher and CLI surfaces:
- top-up submit,
- account inspect,
- hold inspect.
Done Criteria For Sprint 3¶
The settlement-rail slice should be considered closed for hard MVP when:
- ORC arithmetic stays integer-only,
- top-up is idempotent by
gateway-receipt/id, - lazy account semantics are live,
- buyer bridge can create a hold before execution open,
- failed execution open voids the hold,
- settled and refunded outcomes transition holds explicitly,
- dispute freezes the hold instead of silently leaving it active,
- operator read surfaces expose account and hold state without spelunking trace.