Arca Platform Design Axioms
Internal reference for consistent decision-making across the platform. Living document -- extend with new axioms as patterns solidify.
Purpose
This document captures the foundational design axioms that govern how the Arca platform behaves. An axiom here is a principle that:
- Applies globally across the platform (backend, SDK, portal, documentation).
- Resolves ambiguity -- when a developer is unsure how something should work, these axioms provide the answer.
- Must be explicitly overridden -- deviations are allowed but must be documented with rationale.
Audience: Engineers building or extending the Arca platform.
How to use this document:
- Before designing a new feature, scan the axioms for relevant constraints.
- When two reasonable implementations exist, choose the one that aligns with these axioms.
- If a new axiom emerges from a design discussion, add it here.
1Axiom 1: Operation Idempotency Contract
Statement
An operation path names a unique intent. Submitting the same path to the same realm is a declaration that the caller wants the exact same effect. The system MUST behave as follows:
| Prior operation at path? | Inputs match? | System response |
|---|---|---|
| No | N/A | Execute normally |
| Yes | Yes | Return the prior result (safe retry) |
| Yes | No | Reject with a conflict error |
This is the idempotency contract. Every caller-named operation in the platform obeys it.
Motivation
Idempotency exists to solve one problem: safe retry after failure. When a process crashes midway through a workflow, the developer should be able to re-run the entire workflow from the beginning. Operations that already completed return their prior result; operations that didn't complete execute normally. The workflow converges to the same end state regardless of how many times it runs.
But idempotency introduces a second problem: silent intent loss. If a developer accidentally reuses an operation path for a different purpose -- different amount, different source, different target -- and the system silently returns the old result, the developer's second intent is permanently lost without any signal.
The idempotency contract resolves both problems:
- Same path + same inputs = return prior result. This makes retry safe.
- Same path + different inputs = reject. This catches bugs and accidental collisions.
First-Principles Reasoning
Why paths, not auto-generated IDs?
An auto-generated idempotency key (e.g., a UUID minted by the client before each call) is common in REST APIs, but it has a weakness: the key has no semantic meaning. If the client crashes between generating the key and recording it, the key is lost, and retry creates a duplicate operation.
Arca uses paths as idempotency keys because paths are semantic. The path /op/transfer/payroll/jan-2026 describes what the operation IS. The developer can reconstruct it from business logic without needing to remember a random UUID. This means:
- Retry logic can be stateless -- the developer derives the path from the business context.
- Path collisions are meaningful -- they indicate either a retry or a bug, never a random coincidence.
Why compare inputs instead of just deduplicating on path alone?
Consider two scenarios:
Scenario A -- Safe retry:
Call 1: POST /transfer { path: "/op/transfer/payroll/jan", amount: "1000", source: "/treasury", target: "/alice" }
--> 200 OK, operation completed
Call 2: POST /transfer { path: "/op/transfer/payroll/jan", amount: "1000", source: "/treasury", target: "/alice" }
--> 200 OK, returns same operation (idempotent)This is correct. The developer retried after a timeout, and the system correctly returned the prior result.
Scenario B -- Accidental collision:
Call 1: POST /transfer { path: "/op/transfer/payroll/jan", amount: "1000", source: "/treasury", target: "/alice" }
--> 200 OK, operation completed
Call 2: POST /transfer { path: "/op/transfer/payroll/jan", amount: "2000", source: "/treasury", target: "/bob" }
--> ???If the system returns the old result (path-only dedup), the developer believes they sent $2000 to Bob. In reality, $1000 went to Alice and the second call was silently ignored. This is a money-losing bug that violates the platform's core promise.
If the system rejects with a conflict error, the developer immediately knows the path was already used and can investigate. This is safe.
Why not just fail on any duplicate path?
Because that breaks retry. The whole point of idempotency is that f(x) = f(f(x)). If every duplicate path is an error, the developer cannot safely retry after a crash -- they'd need complex logic to distinguish "already succeeded" from "never started." The idempotency contract eliminates this complexity.
Scope of Input Comparison
"Inputs match" means the semantic inputs to the operation are identical. Each operation type defines what constitutes its inputs:
| Operation Type | Compared Fields |
|---|---|
| Transfer | sourceArcaPath, targetArcaPath, amount |
| Create Object | path, type, denomination |
| Create Operation | type, sourceArcaPath, targetArcaPath, input |
| Deposit | N/A (auto-generated paths; see below) |
| Delete | N/A (Temporal workflow dedup; see below) |
The input JSON field stored on every operation provides the raw data for comparison. Implementations may compare the full input JSON or specific extracted fields, but the result must be equivalent.
Sub-Principles
1.1 Path Immutability
Once an operation path is used within a realm, it is permanently consumed. The path cannot be overwritten, amended, or reassigned to a different operation. This holds even if the operation failed -- a failed operation still occupies its path.
Rationale: If failed paths were recyclable, a retry of a failed operation and a genuinely new operation at the same path would be indistinguishable.
1.2 Caller-Owned Naming
The caller chooses the operation path and thus owns the uniqueness guarantee. The platform provides the nonce service as a convenience for generating unique suffixes, but it is not required. Callers may use any naming scheme they prefer.
Conventions:
:separator for operation nonces (e.g.,/op/create/wallets/main:1)-separator for object name nonces (e.g.,/orders/order-47)/(trailing-slash prefix) for child paths (e.g.,/wallets/3)
1.3 Auto-Generated Paths Opt Out of Cross-Call Idempotency
When the system generates a path on the caller's behalf (e.g., deposit operations with auto-incrementing numbers), every call is inherently unique. There is no cross-call idempotency to enforce because the caller never holds the key.
These operations are still internally consistent -- they use database transactions for atomicity -- but they are not retryable via the idempotency contract. If retry semantics are needed, the caller should supply an explicit operation path.
1.4 Two-Tier Dedup for Resource Creation
Object creation has a natural second idempotency layer: the resource path itself. If an active object already exists at the requested path, creation checks type and denomination:
- Match: return existing object (create-if-not-exists / ensure semantics).
- Mismatch: reject with an error.
This is a specialization of the general idempotency contract, not an exception. The resource path acts as an implicit operation path for the "ensure this object exists" intent.
1.5 Database-Enforced Uniqueness
The idempotency contract is backed by a UNIQUE INDEX on (realm_id, path) in the operations table. Application-level checks are optimistic; the database constraint is the ultimate guarantor.
Current Conformance
This section tracks where the codebase aligns with the axiom and where gaps remain. Last reviewed: 2026-02-15
| Endpoint | Path Idempotency | Input Comparison | Conforms? |
|---|---|---|---|
| Create Arca Object (operation path layer) | Yes | No -- returns prior result without input check | Partial |
| Create Arca Object (arca path layer) | Yes | Yes -- validates type and denomination | Yes |
| Transfer | Yes | No -- returns prior result without input check | Partial |
| Create Operation (generic) | Yes | No -- returns prior result without input check | Partial |
| Deposit | Auto-generated path | N/A | Yes (by design) |
| Delete | Temporal workflow ID dedup | N/A | Yes (by design) |
Gap: The operation-path idempotency checks for Create Object (layer 1), Transfer, and Create Operation do not compare inputs. They return the prior result regardless of whether the new request's inputs match. This should be tightened to reject on input mismatch per this axiom.
2Axiom 2: Realm Isolation
Statement
All data is scoped to a realm. No operation, query, or side-effect may cross realm boundaries.
Every table in the data model includes realm_id as part of its primary key or a mandatory foreign key. Queries always filter by realm. There is no API surface that returns data from multiple realms in a single response (the dashboard summary is per-realm, aggregated client-side).
Implications
- Realm deletion destroys all data within it -- objects, operations, events, deltas, balances, nonces.
- Nonce counters are realm-scoped:
/op/transferin realm A and/op/transferin realm B are independent. - Operation paths are unique per realm, not globally.
- Future sharding (Layer 5) will use realm boundaries as the natural partition key.
Full treatment to be written when this axiom requires elaboration.
3Axiom 3: Correlation Spine
Statement
Every state change must be traceable to the operation that caused it. The chain is always: Operation -> Event(s) -> State Delta(s). State deltas attach to events, and events attach to operations. No delta exists without a parent event.
This is the "explainability chain" -- given any balance change or state mutation, a human or system can walk backwards to understand why it happened.
Structure
- Operation: An atomic, immutable record of builder intent (e.g., "transfer $100 from A to B"). Has a lifecycle:
pending->completed|failed|expired. Theinputfield records what was requested; theoutcomefield records the result. Neither is modified after creation (outcome is written once at terminal state). - Event: Records a discrete occurrence within an operation's lifecycle that changed system state. Every state-mutating operation produces at least one event. State deltas attach exclusively to events, never directly to operations.
- State Delta: The before/after record of a specific change (e.g., balance was 500, now 400). Always linked to an event via
event_id. The operation is reachable through the event'soperation_id.
The Event Design Principle
Emit an event when the operation produces a discrete outcome that is independently meaningful to a consumer.
Two tests determine whether a separate event is warranted:
-
Multiplicity test: Can this operation produce multiple discrete outcomes? If yes, each outcome is a separate event. Example: an order operation may produce N fill events over its lifetime. Each fill changes balances independently and carries its own state deltas.
-
Independent observability test: Would a consumer need to react to this occurrence independently of the operation's terminal state? If no one would ever react to an intermediate signal without also reacting to the terminal state, the intermediate signal is noise.
For single-outcome operations (create, transfer, deposit, delete), one terminal event is emitted when the operation completes or fails. This event carries all state deltas produced by that operation step. Intermediate narration events (e.g., "deposit submitted to exchange") that merely restate the operation's progress without carrying independently useful information are omitted.
Event Type Catalog
| Operation | Event(s) | State Deltas Carried |
|---|---|---|
| Create object | object.created | creation |
| Immediate transfer | transfer.completed | balance_change (source + target) |
| Async transfer (initiated) | transfer.initiated | balance_change (source debit) + hold_change (outbound + inbound holds created) |
| Async transfer (outbound settled) | transfer.outbound_released | hold_change (outbound hold released) |
| Async transfer (success) | transfer.completed | balance_change (settlement notation) + hold_change (inbound hold released) |
| Async transfer (failure) | transfer.failed | balance_change (reversal credit) + hold_change (outbound + inbound holds cancelled) |
| Deposit (initiated) | deposit.initiated | hold_change (inbound hold created) |
| Deposit (success) | deposit.completed | balance_change + hold_change (inbound hold released) |
| Deposit (failure) | deposit.failed | hold_change (inbound hold cancelled) |
| Delete (lock acquired) | object.status_changed | status_change |
| Delete (finalized) | object.deleted | status_change + deletion |
| Delete (sweep) | sweep.completed | balance_change (source + target) |
| Delete (exchange withdraw) | withdrawal.completed | balance_change |
| Delete (reverted) | object.status_changed | status_change |
| Delete (liquidation) | liquidation.completed | settlement_change + position_change |
| Order (with fills) | order.filled (per fill) | settlement_change + position_change |
| Order (failed) | order.failed | status_change |
| Order cancel | order.cancelled | (none) |
| Order cancel (failed) | order.cancelled_failed | status_change |
Frontend Consumption Model
A consumer who wants all state changes subscribes to events. Events are the single source of truth for "what changed." The event.created SSE notification tells the consumer something happened; the event's payload and attached deltas tell them what.
- Filter by event type to get specific kinds of changes (e.g.,
order.filledfor trade fills). - Filter by
arca_pathto get changes affecting a specific object. - The operation provides context (what was the builder's intent) but is not needed to understand the change itself.
Implications
- State deltas without an
event_idare illegal. Thestate_deltastable has nooperation_idcolumn; the operation is reachable through the event. - Every state-mutating operation produces at least one event, even synchronous ones (create, immediate transfer). The event is written atomically in the same transaction as the operation and deltas.
- Same-transaction rule for terminal state: Any code that sets an operation to a terminal state (
completed,failed,expired) MUST write at least one event and the appropriate state deltas in the same transaction (or same atomic batch) as the operation state update. No terminal transition may commit without a corresponding event and, where applicable, state deltas. - Direct balance mutations (bypassing the operation/event/delta pipeline) are forbidden.
- The portal Explorer renders the full chain for any selected entity: Operation -> Events -> Deltas.
- Informational events that carry no state deltas (e.g.,
deposit.failed,order.cancelled) are permitted when they communicate a meaningful occurrence to consumers.
Current Conformance
Last reviewed: 2026-02-18
| Operation | Events Emitted | Deltas on Events | Conforms? |
|---|---|---|---|
| Create object | object.created | Yes | Yes |
| Immediate transfer | transfer.completed | Yes | Yes |
| Async transfer | transfer.initiated / transfer.outbound_released / transfer.completed / transfer.failed | Yes (balance_change + hold_change) | Yes |
| Deposit | deposit.initiated / deposit.completed / deposit.failed | Yes (balance_change + hold_change) | Yes |
| Delete lifecycle | object.status_changed, object.deleted, sweep.completed, withdrawal.completed | Yes | Yes |
| Liquidation | liquidation.completed | Yes | Yes |
| Order fill | order.filled | Yes | Yes |
| Order (failed) | order.failed | Yes | Yes |
| Order cancel | order.cancelled | N/A (no deltas) | Yes |
| Order cancel (failed) | order.cancelled_failed | Yes | Yes |
4Axiom 4: Path-Based Identity
Statement
User-facing identity uses hierarchical, human-readable paths. Internal identity uses UUIDs. Both coexist; neither replaces the other.
Path Conventions
| Prefix | Entity Type | Example |
|---|---|---|
/ | Arca object | /wallets/main |
/op/ | Operation | /op/transfer/payroll/jan-2026 |
/ev/ | Event | /ev/deposit/wallets/main/deposit-3/started |
Paths are:
- Immutable once assigned (even after deletion, the path is preserved with a
deleted_attimestamp). - Hierarchical -- slash-delimited segments enable prefix queries and folder-like browsing.
- Scoped to a realm -- the same path can exist in different realms.
UUIDs are:
- Used for internal foreign keys (e.g.,
operation_idon a state delta). - Never shown to developers in the SDK or API responses as primary identifiers (paths are preferred).
- Used for Temporal workflow IDs and other infrastructure-level references.
Full treatment to be written when this axiom requires elaboration.
5Axiom 5: Atomicity Boundaries
Statement
All mutations within a single operation are committed atomically. Cross-operation consistency is eventually consistent.
What is atomic (single Spanner read-write transaction)
- Object creation: object + operation + state delta.
- Transfer: source debit + target credit + operation + two state deltas + two balance upserts.
- Deposit completion: balance update + state delta + completion event + operation state update.
- Delete lock acquisition: status change + pending operation + state delta.
What is eventually consistent
- Deposit initiation -> deposit completion (connected by Temporal workflow with durable timer).
- Delete lock -> delete finalization (connected by Temporal workflow).
- SSE event delivery to subscribers (in-memory broadcast, no persistence guarantee).
Implications
- Within a transaction boundary, there are no partial states visible to other readers.
- Across transaction boundaries, the system may be in intermediate states (e.g.,
pendingdeposit,deletingobject). These states are modeled explicitly and handled in all code paths.
Full treatment to be written when this axiom requires elaboration.
6Axiom 6: Immutability of History
Statement
Operations, events, and state deltas are append-only. Once written, they are never updated or deleted (except as part of realm deletion).
The only mutable fields in the system are:
Operation.state(pending->completed|failed) andOperation.outcome.ArcaObject.status(active->deleting->deleted) andArcaObject.deleted_at.ArcaBalance.amount(materialized cache, updated by new deltas).
Everything else is write-once. The state delta log is the ground truth; balances are derived from it.
Implications
- Corrections are modeled as new operations (e.g., a reversal transfer), not as edits to prior operations.
- Audit trails are inherent -- the delta log IS the audit trail.
- Debugging never requires guessing what happened -- the full history is preserved.
Full treatment to be written when this axiom requires elaboration.
7Axiom 7: Resource-Based Authorization
Statement
Permissions are granted as (action, resource) pairs evaluated against policy statements. Actions are directional and per-resource. No permission is implied across categories. Anything not explicitly allowed is denied.
This is the IAM-style authorization model. It governs scoped JWT tokens (end-user frontends) and scoped API keys (restricted services). Unscoped builder JWTs and unscoped API keys retain full builder-level access.
Structure
A scope contains one or more policy statements, each with an effect:
{
"statements": [
{
"effect": "Allow",
"actions": ["arca:TransferFrom", "arca:ReceiveTo"],
"resources": ["/users/u123/*"]
},
{
"effect": "Allow",
"actions": ["arca:Read"],
"resources": ["*"]
},
{
"effect": "Deny",
"actions": ["arca:*"],
"resources": ["/_internal/*"]
}
]
}- Effect is
Allow(default) orDeny. Deny always overrides Allow (see sub-principle 7.9). - Actions are drawn from the canonical catalog (see
Permission.ktandGET /api/v1/permissions). - Resources are Arca object path patterns: exact paths, prefix globs (
/users/*), or*for everything. - Anything not explicitly allowed is denied (implicit deny). Explicit Deny further overrides any Allow.
Action Catalog
| Category | Action | Description | Checked Against |
|---|---|---|---|
| Lifecycle | arca:CreateObject | Create a new Arca object | Path being created |
| Lifecycle | arca:DeleteObject | Delete an Arca object | Object being deleted |
| Balance | arca:TransferFrom | Debit funds from an object (source side of a transfer) | Source object path |
| Balance | arca:ReceiveTo | Receive funds into an object (deposit, transfer credit, or sweep) | Target object path |
| Balance | arca:WithdrawFrom | Initiate outbound withdrawal from an object | Source object path |
| Read | arca:ReadObject | View object metadata/status | Object path |
| Read | arca:ReadBalance | View object balance | Object path |
| Read | arca:ReadOperation | View operations | Operation path |
| Read | arca:ReadEvent | View events | Event path |
| Read | arca:ReadDelta | View state deltas | Object path |
| Read | arca:Subscribe | SSE real-time stream | Realm-level |
Authorization Principles
These principles resolve ambiguity in all permission-check decisions.
7.1 Deny by Default
If a scoped token or scoped API key does not explicitly grant an action on a resource, the action is denied. This is the foundational rule — there are no implicit permissions.
7.2 Actions are Directional and Per-Resource
For any operation that touches multiple objects, each object is authorized independently against its own path:
- Transfer:
arca:TransferFromon source path,arca:ReceiveToon target path. - Deposit:
arca:ReceiveToon target path. - Delete + sweep:
arca:DeleteObjecton the deleted object's path,arca:ReceiveToon the sweep target's path. - Swap (future): directional actions on each side.
If the caller has permission on the source but not the target, the entire operation fails. This prevents confused-deputy attacks where broad source access inadvertently allows crediting arbitrary destinations.
7.3 Inbound Consolidation, Outbound Separation
Inbound actions are unified. arca:ReceiveTo covers all channels that credit funds to an Arca: deposits (external inbound), transfer credits (internal), and sweeps (deletion lifecycle). The rationale: from the Arca object's perspective, a balance credit is a balance credit. The channel distinction (how the money arrived) is metadata on the operation, not a security-relevant property of the target. A builder who grants arca:ReceiveTo on a path is saying "this Arca may receive funds" -- the mechanism does not change the risk profile of the target.
Outbound actions remain separate. arca:TransferFrom and arca:WithdrawFrom are distinct because they cross genuinely different trust boundaries:
arca:TransferFrommoves funds within the ledger. The money stays in the realm.arca:WithdrawFromcrosses the system boundary to external rails. The money leaves the platform.
These have different risk profiles: an internal transfer is reversible via compensating transaction; a withdrawal may be irreversible once submitted to an external system.
No implied permissions across categories:
arca:ReceiveTodoes not implyarca:TransferFromorarca:WithdrawFrom. Receiving funds is a passive capability; sending funds is an active one with debit risk.arca:TransferFromdoes not implyarca:WithdrawFrom. Internal debit and external withdrawal are separate risk domains.arca:DeleteObjectdoes not implyarca:TransferFrom. Deletion is lifecycle management; debiting is a balance operation. Deleting a zero-balance object requires no transfer permissions.
7.4 Delete + Sweep is Compound Authorization
Deleting an object with non-zero balance requires a sweepToPath. The authorization check is:
arca:DeleteObjecton the object being deleted.arca:ReceiveToon the sweep target path.
If no sweepToPath is provided, only arca:DeleteObject is checked, and the delete fails if balance is non-zero. A scoped token with only arca:DeleteObject can delete zero-balance objects but cannot sweep.
7.5 Resource Matching is Prefix-Based
- Exact:
/treasury/usdmatches only that path. - Prefix:
/users/*matches/users/u123,/users/u123/usd/main, etc. - Global:
*matches everything.
Realm is always a separate constraint, not part of the resource pattern.
7.6 Realm is a Boundary, Not a Resource
Realm scoping is orthogonal to resource patterns. A scoped token is locked to exactly one realm. Resource patterns do not encode realm.
7.7 Wildcard vs. Named Alias Expansion
arca:*is evaluated at check time as "matches any action" (future-proof: new actions added to the catalog are automatically covered).- Named aliases (
arca:Read,arca:Transfer, etc.) are expanded at mint time into their constituent actions and stored in the JWT. This makes token capabilities explicit and auditable, and prevents a future alias redefinition from silently broadening existing tokens.
7.8 Least Privilege by Default
When a builder mints a scoped token, the default scope is arca:Read on * (read-only, all paths) — the minimum useful scope. Write actions must be explicitly granted.
7.9 Explicit Deny Overrides Allow
Policy statements carry an effect field: Allow (default) or Deny.
Evaluation order for each (action, resource) pair:
- If any Deny statement matches the action and resource, the pair is denied — regardless of any Allow statements.
- If any Allow statement matches the action and resource, the pair is granted.
- Otherwise, the pair is denied (implicit deny).
Deny statements serve as safety rails. A builder can add { effect: "Deny", actions: ["arca:*"], resources: ["/_internal/*"] } to guarantee that internal objects are unreachable, even if a broad Allow like arca:* on * is present elsewhere in the scope.
Rationale: Allow-only scopes are sufficient for most use cases, but Deny provides defense-in-depth. When generating scoped tokens dynamically (e.g., from a template), a hard Deny on sensitive paths prevents accidental over-granting. This follows the AWS IAM precedent where explicit deny is the highest-priority evaluation outcome.
Constraint: A scope must contain at least one Allow statement. A pure-Deny scope is rejected at mint time because it would grant no access at all — the implicit deny already covers that.
7.10 Path Traversal Has No Special Semantics
Arca paths are opaque, hierarchical, slash-delimited strings. The segments . (current directory) and .. (parent directory) have no special meaning — they are treated as literal characters, identical to any other path segment.
Security invariant: If any future enhancement proposes adding directory-traversal resolution (e.g., normalizing /users/../admin to /admin), it must undergo a full security analysis of the permission model before proceeding. Path normalization could enable permission bypass: a resource pattern like /users/* would unexpectedly match paths that resolve outside /users/ after traversal. Until such an analysis is completed and documented, path segments are always literal.
Current Conformance
Last reviewed: 2026-02-16
| Surface | IAM Enforcement | Directional Checks | Deny Support | Conforms? |
|---|---|---|---|---|
| Scoped JWT tokens | Yes — policy statements evaluated | Yes — TransferFrom, ReceiveTo | Yes — deny-first evaluation | Yes |
| Scoped API keys | Yes — scope column on api_keys | Yes — same evaluation logic | Yes — same evaluator | Yes |
| Unscoped builder JWT | No scope — full access | N/A | N/A | Yes (by design) |
| Unscoped API keys | No scope — full access | N/A | N/A | Yes (by design) |
POST /transfer | TransferFrom on source, ReceiveTo on target | Yes | Yes | Yes |
POST /objects/delete | DeleteObject on source, ReceiveTo on sweep target | Yes | Yes | Yes |
POST /deposit | ReceiveTo on target | Yes | Yes | Yes |
POST /objects | CreateObject on path | Yes | Yes | Yes |
| Read endpoints | Not yet enforced per-path | — | — | Gap |
Gap: Read endpoints (GET /objects, GET /operations, etc.) do not yet check arca:Read* actions against specific paths. They rely on realm ownership. This should be tightened for scoped tokens to enforce path-level read access.
8Axiom 8: Settlement Lifecycle
Statement
Every fund movement has a settlement lifecycle with bilateral visibility and well-defined terminal states. Pending funds are represented on both the source and target of a movement. Every pending operation must reach a terminal state within a bounded window.
Fund movements in the Arca platform fall on a spectrum from fully atomic (immediate transfers between denominated objects) to multi-step and asynchronous (deposits from external rails, transfers to exchange accounts). Regardless of where a movement falls on this spectrum, the platform enforces consistent semantics for how in-flight funds are represented, how failures are handled, and how the system converges to a terminal state.
Motivation
Without consistent settlement semantics, the same conceptual question -- "where is my money?" -- has different answers depending on which code path was taken. A developer transferring $100 from a wallet to an exchange sees the wallet balance drop but the exchange balance not yet increase. The $100 is invisible. If the exchange deposit fails silently, the money is lost from the developer's perspective: debited from the source, never credited to the target, and no signal that anything went wrong.
This axiom ensures that:
- In-flight funds are always visible on both the sending and receiving side.
- Every async operation has a deadline -- nothing stays pending forever without a signal.
- Failures are reversible by default -- when settlement fails, the system compensates (returns funds to source, clears pending on target).
- Uncertainty is surfaced, not hidden -- when the system cannot determine the outcome of a submitted action, it says so rather than guessing.
Sub-Principles
8.1 Bilateral Visibility
When funds are in flight between two Arca objects, both sides reflect the pending state:
- Source (outbound hold): The source's available balance is reduced by the amount in transit. This is an existing mechanism (
arca_reserved_balanceswith statusheld). The hold prevents double-spending: the reserved amount cannot be used in another operation while the first is settling. - Target (inbound hold): The target shows the expected inbound amount as a separate "pending inbound" figure. This is informational -- the pending amount is not spendable and is not added to the available balance. It exists so that observers of the target Arca can see that funds are on the way.
Balance formulas for any Arca object:
available = settled_balance - sum(outbound_holds)
pending_inbound = sum(inbound_holds)settled_balanceis the materialized amount inarca_balances(already debited by any outbound operations that have started).outbound_holdsare active holds with directionoutboundand statusheld.inbound_holdsare active holds with directioninboundand statusheld.
For deposits (no source Arca): only an inbound hold is created on the target. There is no outbound hold because the funds originate outside the system.
For immediate transfers (atomic, single-transaction): no holds are created. The debit and credit happen in the same Spanner transaction, so there is no intermediate state to represent.
8.2 Terminal State Exhaustiveness
Every asynchronous operation must eventually reach one of three terminal states:
| Terminal State | Meaning | Holds Disposition |
|---|---|---|
completed | Settlement confirmed on both sides | All holds released; target balance credited |
failed | Settlement definitively did not occur or was reversed | Outbound holds cancelled (source re-credited); inbound holds cancelled |
expired | Settlement window exceeded without confirmation | Holds frozen; alert raised; requires manual resolution |
No operation may remain in pending state indefinitely. The settlement window (see 8.3) guarantees eventual transition to a terminal state.
8.2.1 Operation-Hold Consistency
An operation MUST NOT transition to a terminal state (completed, failed, expired) while any of its associated holds remain in held status. Every hold created by an operation must be resolved (released or cancelled) in the same transaction that transitions the operation to its terminal state.
If a terminal transition is attempted with unresolved holds, the transaction MUST be aborted and the operation remains pending.
This is enforced by the assertHoldsResolved guard in ArcaActivitiesImpl, which runs inside the Spanner read-write transaction before mutations are buffered. The guard queries all held holds for the operation and verifies that each one has a corresponding release or cancel mutation in the batch.
Rationale: Orphaned holds are invisible failures. A completed operation with a dangling held hold creates a stuck pending inbound that no workflow will ever resolve, blocks deletion (Axiom 8.7.2), and displays phantom pending amounts. Keeping the operation pending makes the inconsistency visible and diagnosable. This sub-principle mechanically enforces what 8.2's holds disposition table already specifies as required behavior.
Scope: The guard applies to deposit completion/failure and transfer settlement completion/failure. Order operations do not create holds and are not covered (the guard would pass trivially).
8.3 Settlement Window
Every asynchronous operation has a settlement window -- a maximum duration within which the operation is expected to reach a terminal state. If the window elapses without settlement, the operation transitions to expired.
- The settlement window is configured per operation type and channel (e.g., simulated deposits may have a 60-second window; exchange deposits may have a 5-minute window; future ACH rails may have a 3-day window).
- The window is enforced by a Temporal timer that runs in parallel with the settlement workflow. If the timer fires before settlement completes, the workflow transitions the operation to
expiredand emits anoperation.expiredevent. expiredis a terminal state from the workflow's perspective, but it is not final from the platform's perspective. An expired operation may be manually resolved tocompletedorfailedafter investigation.
Rationale: Unbounded pending states are invisible failures. A developer who transfers funds to an exchange and never sees them arrive has no way to know whether the system is still working on it or whether something went wrong. The settlement window converts "silent hang" into "loud alert."
8.4 Failure Reversal (Compensating Transactions)
When an in-flight operation fails, the system executes a compensating transaction -- a new atomic step that undoes the effects of the initiation step:
- Source: Outbound hold cancelled. If the source balance was debited at initiation, a compensating credit restores it. A state delta records the reversal.
- Target: Inbound hold cancelled. No balance change occurs (the target was never credited).
- Operation: State transitions to
failedwith a reason and outcome JSON. - Events: A
*.failedevent is appended to the correlation spine (e.g.,transfer.failed,deposit.failed).
This is a saga-pattern compensation, not a database rollback. The original initiation and the compensation are both preserved as immutable history (Axiom 6). The delta log shows: balance was X, then X-100 (initiation), then X again (compensation). This is auditable and debuggable.
8.5 Channel-Specific Failure Modes
Different settlement channels have different failure characteristics. The platform defines expected behavior for each:
| Channel | Settlement Mode | Failure Modes | Platform Response |
|---|---|---|---|
| Internal transfer (denominated to denominated) | Atomic / Immediate | Cannot partially fail (single Spanner transaction) | N/A -- always succeeds or never starts |
| Simulated deposit | Async (durable timer) | Binary: succeeds or fails after timer | Fail: no balance change, operation marked failed |
| Exchange deposit (transfer to exchange Arca) | Async (workflow + external call) | Rejection by exchange; timeout; permanent loss | Rejection: auto-compensate (cancel holds, re-credit source). Timeout: expire + alert. Loss: manual resolution after expiry |
| Exchange withdrawal (transfer from exchange Arca) | Synchronous (validated against exchange state) | Insufficient withdrawable balance | Rejected at initiation -- no holds created |
| Direct deposit to exchange Arca | Async (workflow + external call) | Same as exchange deposit above | Same as above, but no source to compensate (deposit-only inbound hold cancelled) |
| Future external rails (ACH, wire) | Async (long settlement window) | NSF, fraud, chargeback, timeout | Compensating operations for reversals; expiry for timeouts |
8.6 Uncertainty Handling
When the system submits a request to an external party (e.g., a deposit to a third-party exchange) and cannot determine the outcome -- network failure after submission, ambiguous response, service crash between send and receive -- the operation remains pending until one of:
- Confirmation is received (via polling, callback, or retry) -> transition to
completed. - Rejection is received -> transition to
failedwith compensation. - The settlement window expires -> transition to
expired.
The system never assumes success or failure without evidence. Guessing "it probably worked" risks double-crediting on retry. Guessing "it probably failed" risks losing funds that were actually accepted. expired is the safe default for unknown states -- it freezes the holds, alerts the developer, and awaits human or automated resolution.
Precedent: This follows the "in-doubt transaction" pattern from distributed database literature and the "pending review" state used by payment processors (Stripe, Adyen) when gateway responses are ambiguous.
8.7 Conservation of Value
Within a realm, total economic value is conserved across all internal fund movements. The conservation equation is:
totalEquity = sum(settled_balances) + sum(outbound_holds) + sum(external_positions)Inbound holds are excluded from equity calculations. They are informational -- they show where funds are expected to arrive, but they represent the same economic value already captured by outbound holds on the source side. Adding both would double-count in-flight funds.
Invariant: For any fund movement that does not cross the realm boundary (internal transfers, internal sweeps), totalEquity before the operation equals totalEquity after the operation, at every intermediate state. Only external deposits and withdrawals change totalEquity.
This is effectively double-entry bookkeeping: every internal debit has a matching credit (or pending credit), and in-flight amounts are attributed to exactly one side (the source, via outbound holds).
Testing implication: The conservation invariant is a powerful automated check. For any sequence of internal operations, total equity before and after must be identical. Any discrepancy indicates an accounting bug.
8.7.1 Deletion Conservation
Deleting an Arca object MUST NOT destroy economic value. If the object holds any settled balance, exchange balance, or open positions, the system must either sweep the funds to a specified target (liquidating positions first if needed) or reject the deletion.
The deletion readiness check must aggregate all value sources for the object being deleted:
- Settled balances — the
arca_balancestable. - Exchange balances — the sim-exchange (or production exchange) account's withdrawable balance.
- Open positions — exchange positions that must be liquidated (closed via market order) before the resulting cash can be swept.
If any value source is non-zero and no sweepToPath is provided, the deletion MUST be rejected. For exchange objects with open positions, liquidatePositions=true is also required.
Rationale: This is the bug that motivated the axiom extension. Exchange Arca objects stored their balances in the sim-exchange service, not in arca_balances. The deletion check only inspected arca_balances, so exchange objects with funds could be silently deleted, destroying value. The fix is a unified deletion readiness check that queries all value sources — see documents/axiom-critical-paths.md entry #1.
8.7.2 In-Flight Blocks Deletion
An Arca object with any in-flight operations cannot be deleted. If the object has outbound or inbound holds in held status, the deletion request is rejected. The caller must wait for all pending operations to reach a terminal state (completed, failed, or expired) before requesting deletion.
This prevents two forms of mid-flight corruption:
- Source disappears during pending transfer: The outbound hold references a now-deleted object. The settlement workflow cannot complete or compensate because the source no longer exists.
- Target disappears before funds arrive: The inbound hold references a deleted object. Funds arrive at a non-existent destination.
Both scenarios can cause irrecoverable loss because the settlement workflow has no valid object to credit or debit.
Design note: This is a hard block, not a sweep-able condition. In-flight operations cannot be forcibly cancelled by the deletion path — they must settle naturally. The bounded settlement window (Axiom 8.3) ensures this is not an indefinite wait.
8.7.3 Fee Conservation
Fees and reconciliation adjustments MUST be represented as balance changes to system-owned objects within the realm. System-owned objects live under reserved prefixes (/_system/ and /_builder/), are marked system_owned = true, and may hold negative balances. Builders cannot create, deposit to, or delete objects under either prefix. The /_system/ prefix prohibits all builder-initiated transfers. The /_builder/ prefix allows builders to transfer from (e.g. claiming accumulated builder fees) but prohibits transfers to. The conservation equation totalEquity = sum(all_balances) + sum(outbound_holds) + sum(external_positions) includes system-owned objects and MUST hold exactly for all operations, including fills and fee distributions. System object visibility may be toggled off in the future without changing the conservation model.
System object layout per realm:
/_system/
fees/
exchange -- venue fees (e.g. Hyperliquid's cut), credited on each fill/liquidation
platform -- Arca's platform cut (1bp), credited on each fill/liquidation
adjustments/
{venue} -- reconciliation discrepancies per connected venue
/_builder/
fees -- builder fees (configurable bps), credited on each fill/liquidation; builder can transfer outEnforcement rules:
- Path reservation: The
/_system/and/_builder/prefixes are reserved. Builder-initiatedcreateArcaObjectanddeleteArcaObjectcalls MUST reject paths starting with either prefix.initiateDepositMUST reject targets under either prefix.executeTransferMUST reject any source or target under/_system/, and MUST reject targets (but not sources) under/_builder/. - Auto-creation: When a realm is created (or first accessed), the platform ensures
/_system/fees/exchange,/_system/fees/platform, and/_builder/feesexist as denominated USD objects withsystem_owned = true. The creation uses deterministic IDs and INSERT_OR_UPDATE to be idempotent and retroactive. - Fee recording:
recordFillandrecordLiquidationMUST credit the exchange fee to/_system/fees/exchange, the platform fee to/_system/fees/platform, and the builder fee to/_builder/feesin the same Spanner transaction as the cash delta.distributeBuilderFeeredistributes from/_builder/feesto per-order fee targets if configured. - Negative balances allowed: System objects are exempt from the I4 non-negativity invariant. Adjustment objects can go negative (unexplained value appeared at the venue).
- Double-entry conservation: With fee conservation, the I3 invariant simplifies to pure
sum(all_balance_deltas) = 0for every operation — no special fee exemption needed. - System actor: System-initiated operations on
/_system/*and/_builder/*paths useactorType = "system"and bypass builder IAM policy evaluation. - Future visibility toggle: The
system_ownedcolumn onarca_objectsis the single toggle point. AddingAND system_owned = falseto builder-facing queries hides system objects without changing the conservation model. - Fee failure alerting: Fee crediting and distribution failures MUST be reported to Sentry with
fee_failuretag and the originating operation context. Temporal retries handle transient failures; Sentry captures persistent issues for engineering review.
Rationale: Without fee conservation, exchange fees and platform fees represent value that exits the conservation equation. The I3 invariant must special-case fills with a cash_delta + signedQty × fillPrice = -fee formula. With system objects absorbing fees, every operation becomes pure double-entry bookkeeping: every debit has a matching credit, and the conservation check is a simple sum(deltas) = 0.
Current Conformance
Last reviewed: 2026-02-16
| Mechanism | Bilateral Visibility | Terminal States | Settlement Window | Failure Reversal | Conservation | Conforms? |
|---|---|---|---|---|---|---|
| Simulated deposit | Inbound hold on target | completed / failed | No timeout enforcement | Failed deposits produce no balance change (correct) | N/A (external) | Partial |
| Async transfer (to exchange) | Outbound + inbound holds | completed / failed | No timeout enforcement | Compensating transaction on failure | Outbound holds included in equity (correct) | Partial |
| Immediate transfer | N/A (atomic) | completed only | N/A | N/A | Balanced debit/credit (correct) | Yes |
| Path aggregation | Shows outbound + inbound holds | N/A | N/A | N/A | Outbound holds added to equity, inbound excluded (correct) | Yes |
| Object deletion (denominated) | N/A | completed | N/A | N/A | Sweep required if balance > 0, in-flight blocks deletion (correct) | Yes |
| Object deletion (exchange) | N/A | completed / failed | 5-minute workflow timeout | Revert to active on timeout | Checks exchange balance + positions + in-flight holds (correct) | Yes |
| Operation-hold consistency (8.2.1) | N/A | Guard enforced on all deposit + transfer terminal transitions | N/A | N/A | Prevents orphaned holds that distort pending inbound display | Yes |
| Fee conservation (8.7.3) | N/A | N/A | N/A | N/A | Exchange fees → /_system/fees/exchange, platform fees → /_system/fees/platform, builder fees → /_builder/fees. Failures reported to Sentry. | Yes |
Gaps:
- No settlement window enforcement on transfers/deposits -- a failed external call causes Temporal retries forever or silent failure.
- The
expiredoperation state exists in the enum but is not yet enforced via Temporal parallel timer.
Changelog
| Date | Change |
|---|---|
| 2026-02-15 | Initial document. Axiom 1 (Operation Idempotency) fully elaborated. Axioms 2-6 stubbed. |
| 2026-02-15 | Added Axiom 7 (Resource-Based Authorization). Full elaboration with action catalog, 8 sub-principles, and conformance table. |
| 2026-02-15 | Added sub-principles 7.9 (Explicit Deny Overrides Allow) and 7.10 (Path Traversal Has No Special Semantics). |
| 2026-02-16 | Added Axiom 8 (Settlement Lifecycle). Full elaboration with 7 sub-principles: bilateral visibility, terminal states, settlement window, failure reversal, channel-specific failure modes, uncertainty handling, and conservation of value. |
| 2026-02-16 | Consolidated inbound permissions: replaced arca:DepositTo, arca:TransferTo, arca:SweepTo with unified arca:ReceiveTo. Updated Axiom 7 action catalog, sub-principles 7.2-7.4, and conformance table. |
| 2026-02-17 | Added sub-principles 8.7.1 (Deletion Conservation) and 8.7.2 (In-Flight Blocks Deletion). Updated conformance table with object deletion rows. |
| 2026-02-18 | Fully elaborated Axiom 3 (Correlation Spine). Codified event design principle (multiplicity + independent observability tests). All state deltas now flow through events; operation_id removed from state_deltas table. Added event type catalog, frontend consumption model, and conformance table. |
| 2026-02-18 | Added sub-principle 8.2.1 (Operation-Hold Consistency). Guard prevents terminal state transitions with unresolved holds. Added hold_change delta type. Updated Axiom 3 event catalog with deposit.initiated, transfer.initiated, transfer.outbound_released events carrying hold_change deltas. |
| 2026-02-18 | Added canonical ledger implementation guidance: append-only balance/position ledgers with current projections, plus transactional outbox for reliable event fanout. Snapshot-as-of reads now derive from ledger history. |
| 2026-02-21 | Added sub-principle 8.7.3 (Fee Conservation). System-owned objects under /_system/ absorb exchange fees, platform fees, and reconciliation adjustments. Conservation equation becomes pure double-entry. Updated conformance table. |
| 2026-02-22 | Updated 8.7.3 to document /_builder/fees system object (builder fees), /_builder/ path reservation rules, idempotent/retroactive auto-creation, and fee failure alerting via Sentry. |