Arca/Documentation
Join Waitlist

Installation & Setup

The ArcaSDK Swift package provides a native iOS/macOS client for the Arca platform. It uses Swift structured concurrency (async/await), Codable models, and actor-based thread safety. Requires iOS 15+ / macOS 12+. Zero third-party dependencies.

Installation (Swift Package Manager)

Add the package dependency in your Package.swift or via Xcode:

swift
// Package.swift
dependencies: [
.package(url: "https://github.com/arcaresearch/arca-swift-sdk.git", from: "0.1.0"),
],
targets: [
.target(name: "MyApp", dependencies: ["ArcaSDK"]),
]

Or in Xcode: File → Add Package Dependencies → enter the repository URL. If SPM reports no versions available, use Branch: main instead of a version rule.

Initialization

The Swift SDK authenticates with a scoped JWT token minted by your backend. The realm is extracted from the token claims automatically.

swift
import ArcaSDK
// Recommended: with automatic token refresh
let arca = try Arca(
token: scopedJwt,
tokenProvider: {
try await myBackend.getArcaToken()
}
)
// Or provider-only (fetches the first token automatically)
let arca = try await Arca.withTokenProvider {
try await myBackend.getArcaToken()
}
// Static token (manual refresh via updateToken())
let arca = try Arca(token: scopedJwt)
// Explicit realm override (if the token doesn't contain realmId)
let arca = try Arca(token: scopedJwt, realmId: "rlm_01abc")

Token Refresh

Scoped tokens are short-lived (default 60 min). The SDK provides two approaches to handle expiry:

Recommended: Token Provider (automatic)

Pass a tokenProvider closure that calls your backend to mint a fresh token. The SDK handles the rest:

  • Proactive refresh — ~30 seconds before expiry
  • 401 retry — if a request fails with 401, the SDK calls the provider, swaps the token, and retries the request once
  • WebSocket — on reconnect, the provider is called for a fresh credential

Concurrent refresh calls are deduplicated — only one in-flight request at a time.

Manual: updateToken()

If you prefer full control, use updateToken() to swap the token yourself. If the WebSocket is disconnected, it reconnects immediately with the new token.

swift
await arca.updateToken(newScopedJwt)

Auth Error Event

Register an onAuthError listener to handle unrecoverable authentication failures — e.g., when a 401 is received and no provider is configured, or when the provider itself throws.

swift
let id = await arca.onAuthError { error in
showSessionExpiredUI()
}
// Remove the listener when no longer needed
await arca.removeAuthErrorHandler(id: id)

History Cache

The SDK caches responses from getEquityHistory(), getPnlHistory(), and getCandles() in an in-memory LRU cache. This eliminates redundant network calls when switching between charts or toggling intervals. The watchEquityChart(), watchPnlChart(), and watchCandleChart() methods also benefit since they call the history methods internally.

By default, the cache holds up to 50 entries. You can customize this via the cache parameter:

swift
// Custom cache size
let arca = try Arca(token: scopedJwt, cache: CacheConfig(maxEntries: 100))
// Disable caching entirely
let arca = try Arca(token: scopedJwt, cache: .disabled)
// Manually clear the cache (e.g. after a known data change)
await arca.clearHistoryCache()

Authentication Model

The Swift SDK is designed for frontend/mobile apps. It authenticates exclusively with scoped JWT tokens minted by your backend via POST /auth/token. No API key auth or admin operations are supported — those are the responsibility of your backend. See Architecture Patterns for guidance on which operations to expose to the frontend via scoped tokens versus routing through your backend.

If the token does not embed a realmId claim, provide it explicitly in the initializer.

Operation Handles

All mutation methods (ensureDenominatedArca, fundAccount, transfer, placeOrder, etc.) return an OperationHandle<T> — a handle that starts the HTTP call immediately and provides progressive disclosure of the operation lifecycle.

Simple: One-liner to Settlement

swift
// Await full settlement in one line
try await arca.fundAccount(arcaRef: "/wallets/main", amount: "1000").settled

Progressive Disclosure

swift
let handle = arca.fundAccount(arcaRef: "/wallets/main", amount: "1000")
// Get the HTTP response immediately (operation may still be pending)
let response = try await handle.submitted
print(response.poolAddress ?? "no pool")
// Wait for full settlement
try await handle.settled
// Or with an explicit timeout
try await handle.settled(timeoutSeconds: 15)

Batching with async let

swift
async let d1 = arca.fundAccount(arcaRef: "/wallets/main", amount: "500").settled
async let d2 = arca.fundAccount(arcaRef: "/wallets/savings", amount: "300").settled
let (r1, r2) = try await (d1, d2)

OrderHandle

placeOrder returns an OrderHandle with additional exchange lifecycle methods for fills and cancellation:

swift
let order = arca.placeOrder(
path: "/op/order/btc-1", objectId: exchangeId,
coin: "hl:BTC", side: .buy, orderType: .market, size: "0.01"
)
// Wait for placement
try await order.settled
// Wait for fill
let filled = try await order.filled(timeoutSeconds: 30)
print(filled.order.avgFillPrice ?? "")
// Stream fills as they arrive
for try await fill in order.fills() {
print("Filled \(fill.size) @ \(fill.price)")
}
// Get structured fill summary (P&L, fee breakdown, direction, resulting position)
let summary = try await order.fillSummary()
print(summary?.realizedPnl ?? "", summary?.dir ?? "", summary?.feeBreakdown ?? "")
// Callback-based fills
let unsub = order.onFill { fill in
print("Got fill: \(fill.size)")
}
// later: unsub()
// Cancel the order
try await order.cancel().settled

API Summary

Property / MethodReturnsDescription
.submittedResponseHTTP response before settlement
.settledResponseWait for full settlement
.settled(timeoutSeconds:)ResponseWait with explicit timeout
.filled(timeoutSeconds:)SimOrderWithFillsWait for order fill (OrderHandle only)
.fillSummary(timeoutSeconds:)Fill?Get fill with P&L, fee breakdown, direction (OrderHandle only)
.fills(timeoutSeconds:)AsyncThrowingStream<SimFill>Stream fills (OrderHandle only)
.onFill(callback)() -> VoidCallback per fill, returns unsub closure (OrderHandle only)
.cancel(path:)OperationHandleCancel the order (OrderHandle only)

Behind the scenes, settlement waiting uses WebSocket operation.updated events for real-time detection with periodic HTTP polling as a safety net — no manual polling required.

Arca Objects

ensureDenominatedArca

Ensure a denominated Arca object exists at the given path. Creates it if it doesn't exist; returns the existing one if it does (matching type and denomination). Safe to call on every request.

Returns an OperationHandle — use .settled to wait for the object to be active (immediate for existing objects).

swift
try await arca.ensureDenominatedArca(
ref: "/wallets/main",
denomination: "USD",
operationPath: "/op/create/wallets/main:1" // optional idempotency key
).settled
refStringrequired
Full Arca path.
denominationStringrequired
Currency or asset denomination (e.g., USD, BTC).
metadataString?
Optional JSON metadata string.
operationPathString?
Operation path for explicit idempotency (use the nonce API to generate).

ensureArca

Ensure an Arca object of any type exists at the given path. Creates if missing, returns existing if matching. Returns an OperationHandle.

swift
try await arca.ensureArca(
ref: "/deposits/d1",
type: .deposit,
denomination: "USD"
).settled
refStringrequired
Full Arca path.
typeArcaObjectTyperequired
One of: .denominated, .exchange, .deposit, .withdrawal, .escrow.
denominationString?
Required for .denominated type.
metadataString?
Optional JSON metadata string.
operationPathString?
Optional operation-level idempotency key.

getObject / listObjects

swift
let obj = try await arca.getObject(path: "/wallets/main")
print(obj.denomination, obj.status)
let list = try await arca.listObjects(prefix: "/wallets")
print(list.total)

getObjectDetail

Get full object detail including operations, events, state deltas, and balances.

swift
let detail = try await arca.getObjectDetail(objectId: obj.id.rawValue)
print(detail.operations.count)
print(detail.balances)
print(detail.reservedBalances)

getBalances / getBalancesByPath

swift
let balances = try await arca.getBalances(objectId: obj.id.rawValue)
for balance in balances {
print(balance.denomination, balance.amount)
}
// Or by path
let balances = try await arca.getBalancesByPath(path: "/wallets/main")

browseObjects

S3-style hierarchical browsing. Returns folders (path prefixes) and objects at the given level.

swift
let result = try await arca.browseObjects(prefix: "/users/")

getObjectVersions

Get all versions of an Arca object at the same path (for deleted + recreated objects).

swift
let versions = try await arca.getObjectVersions(objectId: obj.id.rawValue)

getSnapshotBalances

Get historical balances and positions for an object at a specific timestamp.

swift
let snapshot = try await arca.getSnapshotBalances(
objectId: obj.id.rawValue,
asOf: "2026-02-18T18:00:00Z"
)

ensureDeleted

Delete an Arca object by path. Returns an OperationHandle. If the object has remaining balances, provide a sweep target. For exchange objects with open positions, set liquidatePositions: true to close them via market order first.

swift
// Simple deletion
try await arca.ensureDeleted(ref: "/wallets/old").settled
// Deletion with balance sweep
try await arca.ensureDeleted(
ref: "/wallets/old",
sweepTo: "/wallets/main"
).settled
// Exchange deletion with position liquidation
try await arca.ensureDeleted(
ref: "/exchanges/hl1",
sweepTo: "/wallets/main",
liquidatePositions: true
).settled
refStringrequired
Full Arca path to delete.
sweepToString?
Arca path to sweep remaining funds into before deletion.
liquidatePositionsBool
Close open exchange positions before deletion. Default: false.

Transfers & Fund Account

transfer

Execute a transfer between two Arca objects. Returns an OperationHandle. The operation path serves as the idempotency key.

swift
let result = try await arca.transfer(
path: "/op/transfer/alice-to-bob-1",
from: "/wallets/alice",
to: "/wallets/bob",
amount: "250.00"
).settled
print(result.operation.state) // .completed
pathStringrequired
Operation path (idempotency key). Must be unique per realm.
fromStringrequired
Source Arca object path to debit.
toStringrequired
Target Arca object path to credit.
amountStringrequired
Transfer amount as a decimal string.

fundAccount

Fund an Arca object (denominated or exchange). Returns an OperationHandle — use .settled to wait for settlement, or .submitted for the immediate response (includes pool address for on-chain funding). For exchange objects, the deposit is forwarded to the venue after on-chain confirmation.

Settlement behavior depends on realm type:

  • Demo / development / testing: Auto-mints simulated tokens and settles within seconds.
  • Production: Creates a deposit intent with a poolAddress. The operation stays pending until real USDC arrives on-chain.

For user-facing deposit flows in production, use createPaymentLink() instead.

swift
// Wait for full settlement
try await arca.fundAccount(arcaRef: "/wallets/main", amount: "1000").settled
// Or get the HTTP response first, then wait
let handle = arca.fundAccount(arcaRef: "/wallets/main", amount: "1000")
let response = try await handle.submitted
print(response.poolAddress ?? "simulated")
try await handle.settled
arcaRefStringrequired
Target Arca object path.
amountStringrequired
Amount as a decimal string.

defundAccount

Withdraw funds from an Arca object to an external on-chain address. Returns an OperationHandle. When on-chain custody is active, funds are sent to the destination address. For exchange objects, first transfer funds to a denominated object, then defund from there. This is a developer tool — for production withdrawal flows, use createPaymentLink().

swift
try await arca.defundAccount(
arcaPath: "/wallets/main",
amount: "500.00",
destinationAddress: "0xabc..."
).settled
arcaPathStringrequired
Source Arca object path.
amountStringrequired
Amount as a decimal string.
destinationAddressString?
On-chain destination address.

Operations & Events

getOperation

Get operation detail by ID, including correlated events and state deltas.

swift
let detail = try await arca.getOperation(operationId: "op_01abc")
print(detail.operation.type, detail.operation.state)
print(detail.events.count, detail.deltas.count)

listOperations

List operations in the realm, optionally filtered by type.

swift
// Single type
let list = try await arca.listOperations(type: .transfer)
// Multiple types with inline context
let history = try await arca.listOperations(
types: [.fill, .transfer],
includeContext: true
)
typeOperationType?
Filter by a single type: .transfer, .create, .delete, .deposit, .withdrawal, .order, .cancel.
types[OperationType]?
Filter by multiple types. Takes precedence over type.
includeContextBool
When true, each operation includes its typed context inline (transfer amount/fee, fill details, etc.).

Display Helpers

Convenience methods for rendering operation history in strategy UIs.

swift
// Transfer direction relative to an object
let dir = op.transferDirection(for: "/exchanges/strat-1") // .incoming or .outgoing
// Friendly counterparty label
let label = op.counterpartyLabel(for: "/exchanges/strat-1") // "Vault"
// Context accessors
let amount = op.context?.transferAmount // "5000"
let fee = op.context?.transferFee // "0.05"
let denom = op.context?.transferDenomination // "USD"

listEvents / getEventDetail

swift
let events = try await arca.listEvents()
let detail = try await arca.getEventDetail(eventId: events.events[0].id.rawValue)

listDeltas

List state deltas for a given Arca path (before/after values for every state change).

swift
let deltas = try await arca.listDeltas(arcaPath: "/wallets/main")

nonce

Reserve the next unique nonce for a path prefix. Returns a path suitable for use as an idempotency key.

swift
let nonce = try await arca.nonce(prefix: "/op/transfer/fund")
try await arca.transfer(
path: nonce.path,
from: "/wallets/alice",
to: "/wallets/bob",
amount: "100"
).settled

summary

Get aggregate counts for the realm.

swift
let summary = try await arca.summary()
print(summary.objectCount, summary.operationCount, summary.eventCount)

waitForOperation

Wait for a specific operation to reach a terminal state. Uses WebSocket operation.updated events for real-time settlement detection with periodic HTTP polling as a safety net. In most cases, you should use .settled on an OperationHandle instead.

swift
do {
let completed = try await arca.waitForOperation(
operationId: "op_01abc",
timeoutSeconds: 30
)
print(completed.state) // .completed
} catch ArcaError.operationFailed(let op) {
print("Failed:", op.failureMessage ?? op.state.rawValue)
}

This is useful when your backend initiates an operation (e.g. via the TypeScript SDK) and your client needs to observe the outcome. Pass the operationId from the backend response and call waitForOperation on the client — it resolves when the operation settles and throws ArcaError.operationFailed(operation:) on failure. The failureMessage field contains a human-readable reason.

Aggregation & P&L

Prefix vs. Exact Path

All aggregation methods accept a prefix parameter. A trailing slash aggregates all objects under that path (e.g., /users/alice/ returns all of Alice's objects). Omitting the trailing slash targets a single exact object (e.g., /users/alice/exchanges/hl1 returns only that object).

getPathAggregation

Get aggregated valuation for objects matching the prefix (or exact path).

swift
// Aggregate all objects under a prefix
let agg = try await arca.getPathAggregation(prefix: "/users/alice/")
print(agg.totalEquityUsd, agg.breakdown.count)
// Single object
let single = try await arca.getPathAggregation(prefix: "/users/alice/exchanges/hl1")
// Historical aggregation at a past timestamp
let hist = try await arca.getPathAggregation(
prefix: "/users/",
asOf: "2026-02-15T00:00:00Z"
)

Returns a PathAggregation:

prefixString
The prefix or path that was queried.
totalEquityUsdString
Total equity in USD (2 decimal places).
departingUsdString
Total departing (outbound held) balance in USD. Pass-through: already USD-denominated, not recomputed from mid prices.
arrivingUsdString?
Total arriving (inbound held) balance in USD. Pass-through: already USD-denominated, not recomputed from mid prices.
breakdown[AssetBreakdown]
Per-asset breakdown with amount, price, and USD value.
asOfString?
Timestamp of the historical snapshot (omitted for live queries).
cumInflowsUsdString?
Cumulative inflows in USD since the flowsSince time (only present when a watch was created with flowsSince).
cumOutflowsUsdString?
Cumulative outflows in USD since the flowsSince time (only present when a watch was created with flowsSince).

Per-object valuations are not included in the REST response. Use getObjectValuation(path:) for a one-shot read, or watchObject(path:exchange:) / watchObjects(paths:exchange:) for streaming.

getPnl

Get P&L for objects matching the prefix (or exact path) over a time range.

swift
let pnl = try await arca.getPnl(
prefix: "/users/alice/",
from: "2026-02-01T00:00:00Z",
to: "2026-03-01T00:00:00Z"
)
print(pnl.pnlUsd) // e.g. "1200.00"

Returns a PnlResponse:

prefixString
The prefix or path that was queried.
fromString
Start of the time range (RFC 3339).
toString
End of the time range (RFC 3339).
startingEquityUsdString
Equity at the start of the range.
endingEquityUsdString
Equity at the end of the range.
netInflowsUsdString
Total inflows (deposits + inbound transfers) in USD.
netOutflowsUsdString
Total outflows (outbound transfers) in USD.
pnlUsdString
Calculated P&L: endingEquity - startingEquity - netInflows + netOutflows.
externalFlows[ExternalFlowEntry]
Itemized list of deposits and transfers that crossed the prefix boundary.

getEquityHistory

Get equity time-series sampled evenly over a time range (default 200 points, max 1000).

swift
let history = try await arca.getEquityHistory(
prefix: "/users/",
from: "2026-02-01T00:00:00Z",
to: "2026-03-01T00:00:00Z",
points: 100
)

Returns an EquityHistoryResponse:

prefixString
The prefix or path that was queried.
fromString
Start of the time range.
toString
End of the time range.
pointsInt
Actual number of data points returned.
equityPoints[EquityPoint]
Time-series array. Each entry has timestamp (String) and equityUsd (String, 2 decimal places).

Portfolio Aggregation Watches

Aggregation watches provide real-time portfolio valuation for any combination of Arca objects: total equity, asset breakdown, and pass-through in-transit totals. Structural updates stream as PathAggregation snapshots; for per-object detail, use watchObject(path:exchange:) or watchObjects(paths:exchange:).

createAggregationWatch

Create a watch that tracks objects matching the given sources. Returns the watch ID and an initial aggregation snapshot. After creation, listen for aggregation.updated events via the WebSocket event stream.

swift
// Track all objects under a user prefix
let watch = try await arca.createAggregationWatch(sources: [
AggregationSource(type: .prefix, value: "/users/alice/"),
])
print(watch.aggregation.totalEquityUsd)
// Combine multiple source types
let combo = try await arca.createAggregationWatch(sources: [
AggregationSource(type: .prefix, value: "/users/alice/"),
AggregationSource(type: .paths, value: "/treasury/usd,/treasury/btc"),
AggregationSource(type: .pattern, value: "/users/*/exchanges/hl/*"),
])

Source types: .prefix (path prefix), .pattern (glob with * wildcard), .paths (comma-separated exact paths), .watch (compose another watch by ID).

watchAggregation(sources:exchange:flowsSince:)

Subscribe to real-time aggregation updates with a single call. Internally creates a server-side watch, watches for structural change events and mid prices, and performs client-side revaluation so every emission reflects live prices. Call stop() when done — it cleans up everything automatically.

Pass flowsSince (ISO 8601 timestamp) to receive cumulative inflow/outflow values (cumInflowsUsd/cumOutflowsUsd) computed from that point forward on every aggregation update.

swift
let stream = try await arca.watchAggregation(sources: [
AggregationSource(type: .prefix, value: "/users/alice/"),
])
for await agg in stream.updates {
print("Equity:", agg.totalEquityUsd)
print("Breakdown entries:", agg.breakdown.count)
}
// With flow tracking from a specific time
let flowStream = try await arca.watchAggregation(
sources: [AggregationSource(type: .prefix, value: "/users/alice/")],
flowsSince: "2026-03-01T00:00:00Z"
)
for await agg in flowStream.updates {
print("Inflows:", agg.cumInflowsUsd ?? "n/a")
print("Outflows:", agg.cumOutflowsUsd ?? "n/a")
}
// Clean up when done
await stream.stop()

The stream emits on both structural changes (fills, balance updates, object creation/deletion) and mid-price ticks. The aggregation property always holds the latest snapshot.

Manual Wiring (Advanced)

For full control you can use createAggregationWatch() with manual WebSocket and price subscriptions.

swift
// 1. Create the watch
let watch = try await arca.createAggregationWatch(sources: [
AggregationSource(type: .prefix, value: "/users/alice/"),
])
var portfolio = watch.aggregation
// 2. Listen for structural updates
let aggStream = await arca.ws.aggregationEvents()
Task {
for await (_, agg, _) in aggStream {
if let agg = agg {
portfolio = agg
}
}
}
// 3. Revalue on mid price ticks
let prices = try await arca.watchPrices()
for await mids in prices.updates {
portfolio = portfolio.revalued(with: mids)
renderPortfolio(portfolio)
}

getWatchAggregation / destroyAggregationWatch

swift
// Get current aggregation on demand
let agg = try await arca.getWatchAggregation(watchId: watch.watchId)
// Destroy when done (also auto-evicted after 5 min of inactivity)
try await arca.destroyAggregationWatch(watchId: watch.watchId)

watchPnlChart(path:from:to:points:exchange:anchor:)

Create a live P&L chart that merges historical P&L data with real-time aggregation updates and operation events. The anchor parameter controls the y-axis baseline:

anchorPnlAnchor
.zero (default) for standard P&L starting at 0, or .equity to shift the chart so the live (rightmost) value equals the current account equity. When set to .equity, each PnlPoint includes a valueUsd field suitable for the chart y-axis. Historical points remain stable during price movements.
swift
// Equity-anchored P&L chart (portfolio value view)
let chart = try await arca.watchPnlChart(
path: "/users/alice/",
from: "2026-02-01T00:00:00Z",
to: "2026-03-01T00:00:00Z",
anchor: .equity
)
for await update in chart.updates {
// Use valueUsd for the y-axis — the live point equals current equity
for point in update.points {
print(point.timestamp, point.valueUsd ?? point.pnlUsd)
}
}

Exchange (Perps)

Trade simulated perpetual futures on exchange Arca objects. These methods wrap the sim-exchange API and follow the same idempotency contract as other operations.

ensurePerpsExchange

Ensure a perps exchange Arca object exists at the given path. Returns an OperationHandle. Denomination is automatically set to USD.

swift
try await arca.ensurePerpsExchange(ref: "/exchanges/hl1").settled
refStringrequired
Full Arca path for the exchange object.
operationPathString?
Optional idempotency key.

getExchangeState

Get exchange account state including equity, margin, positions, and open orders. Use marginSummary for account equity and available balance — see getActiveAssetData below for computing max order sizes. The marginSummary fields are the authoritative source for exchange account balances — there is no separate usdBalance property exposed.

swift
let state = try await arca.getExchangeState(objectId: exchangeId)
print(state.marginSummary.equity) // total equity (deposited + unrealized PnL)
print(state.marginSummary.totalRawUsd) // total deposited USD (before PnL)
print(state.marginSummary.availableToWithdraw) // withdrawable (equity minus maintenance margin)
print(state.positions)
print(state.openOrders)

updateLeverage / getLeverage

Set or get the leverage for a coin. Leverage is a per-coin setting, not per-order.

swift
// Set leverage
let result = try await arca.updateLeverage(
objectId: exchangeId,
coin: "hl:BTC",
leverage: 10
)
// Get leverage for a coin
let setting = try await arca.getLeverage(objectId: exchangeId, coin: "hl:BTC")
objectIdStringrequired
Exchange Arca object ID.
coinStringrequired
Coin to set leverage for.
leverageIntrequired
Leverage multiplier (1 to maxLeverage).

placeOrder

Place a market or limit order. Returns an OrderHandle with full order lifecycle methods (see Operation Handles). If leverage is omitted, the order uses the account's current per-coin leverage setting.

swift
let order = arca.placeOrder(
path: "/op/order/btc-buy-1",
objectId: exchangeId,
coin: "hl:BTC",
side: .buy,
orderType: .market,
size: "0.01",
leverage: 5
)
try await order.settled // wait for placement
let filled = try await order.filled(timeoutSeconds: 30) // wait for fill
for try await fill in order.fills() { /* ... */ } // stream fills
try await order.cancel().settled // cancel

Close Position (Market, close-only)

For exits, set reduceOnly: true and omit leverage. Close-only paths should not trigger leverage changes.

swift
try await arca.placeOrder(
path: "/op/order/btc-close-1",
objectId: exchangeId,
coin: "hl:BTC",
side: .sell,
orderType: .market,
size: "0.01",
reduceOnly: true
).settled

Reverse After Exit (two-step)

If you support reversal UX, do it in two operations: close-only first, then a separate opposite-side order. Do not submit a single order at 2× the position size — this will fail with an insufficient-margin error when the position is underwater, because the realized loss on close consumes the released margin and leaves nothing for the new leg. The two-step approach guarantees the close always succeeds (reduce-only orders skip the margin check), and the second leg can be evaluated independently.

To determine whether a reverse is feasible before showing the option, compare the streamed maxSellSize / maxBuySize from watchMaxOrderSize() against twice the exit size. If maxSellSize < 2 × exitSize (for a long), disable the reverse toggle — the account doesn’t have enough margin to open the opposite position after closing.

pathStringrequired
Operation path (idempotency key).
objectIdStringrequired
Exchange Arca object ID.
coinStringrequired
Coin to trade (e.g., BTC, ETH).
sideOrderSiderequired
Order side: .buy or .sell.
orderTypeOrderTyperequired
Order type: .market or .limit.
sizeStringrequired
Order size as a decimal string.
priceString?
Limit price. Required for .limit orders.
leverageInt?
Optional leverage override. When provided, sets the account's per-coin leverage before placing the order and persists for subsequent orders on this coin. Omit to use the current setting.
reduceOnlyBool?
Only reduce an existing position. Default: false.
isTriggerBool?
If true, place a trigger order (TP/SL). When set, provide triggerPx and tpsl.
triggerPxString?
Mark price threshold that activates the order.
isMarketBool?
If true, execute as market when triggered; if false, use price as the limit after trigger.
tpslTpslType?
.takeProfit ("tp") or .stopLoss ("sl"). Required when isTrigger is true.
groupingTpslGrouping?
.standalone ("na") — standalone trigger; .normalTpsl — fixed-size, parent-linked (OCO: one fires, siblings cancel); .positionTpsl — auto-resizes with position; cancels when position closes.

Order status includes .waitingForTrigger and .triggered alongside the usual cases (open, filled, etc.).

listOrders / getOrder / cancelOrder

swift
// List orders
let orders = try await arca.listOrders(objectId: exchangeId)
// Get a specific order with fill history
let detail = try await arca.getOrder(objectId: exchangeId, orderId: orderId)
print(detail.order.status, detail.fills.count)
// Cancel an open order (returns OperationHandle)
try await arca.cancelOrder(
path: "/op/cancel/btc-1",
objectId: exchangeId,
orderId: orderId
).settled

placeTwap / cancelTwap / getTwap / listTwaps

TWAP (Time-Weighted Average Price) orders execute a total size over a duration by placing market order slices at regular intervals. Duration up to 30 days. Active TWAPs are auto-cancelled on account liquidation.

swift
// Start a TWAP
let twap = try await arca.placeTwap(
exchangeId: exchangeId,
path: "/wallets/main/twap/btc-accumulate",
coin: "hl:BTC",
side: .buy,
totalSize: "10.0",
durationMinutes: 120,
intervalSeconds: 30
)
print(twap.twap.status) // .active
// Cancel
try await arca.cancelTwap(exchangeId: exchangeId, operationId: twap.operationId)
// Get status
let status = try await arca.getTwap(exchangeId: exchangeId, operationId: twap.operationId)
print(status.twap.executedSize, status.twap.filledSlices)
// List active TWAPs
let active = try await arca.listTwaps(exchangeId: exchangeId, activeOnly: true)
// Get limits (local, no network)
let limits = arca.twapLimits

listPositions

Each position includes returnOnEquity (unrealized P&L / margin used) and positionValue (size × mark price), computed server-side.

swift
let positions = try await arca.listPositions(objectId: exchangeId)
for pos in positions {
print(pos.coin, pos.side, pos.size, pos.unrealizedPnl ?? "0")
print("ROE:", pos.returnOnEquity ?? "N/A", "Value:", pos.positionValue ?? "N/A")
}

getActiveAssetData

Get active trading data for a coin on an exchange object. Returns a one-shot snapshot of max order sizes. For live updates that react to margin, position, and price changes, use watchMaxOrderSize() instead.

swift
let data = try await arca.getActiveAssetData(objectId: exchangeId, coin: "hl:BTC")
// Use maxBuySize / maxSellSize for order size sliders
let maxLong = Double(data.maxBuySize) ?? 0
let maxShort = Double(data.maxSellSize) ?? 0
// Pass leverage to override the stored setting
let data10x = try await arca.getActiveAssetData(
objectId: exchangeId, coin: "hl:BTC", leverage: 10
)
coinString
Asset name.
leverageLeverageInfo
Current leverage setting. type is .cross or .isolated.
maxBuySizeString
Max buy size in tokens (positive). Accounts for margin, leverage, and existing positions.
maxSellSizeString
Max sell size in tokens (positive). Accounts for margin, leverage, and existing positions.
maxBuyUsdString
Max buy size in USD (positive).
maxSellUsdString
Max sell size in USD (positive).
availableToTradeString
Raw available margin in USD (equity minus margin in use). Direction-agnostic — use for "buying power" display. For per-side max exposure, use maxBuyUsd/maxSellUsd.
markPxString
Current mark price as a decimal string.
feeRateString
All-in fee rate as a decimal (e.g. "0.00045" for 4.5 bps). Includes exchange taker fee, platform fee, and builder fee.

Arca.orderBreakdown(options:)

Pure static calculator (no network call) that converts between three order input modes. Use it to show fee previews and order breakdowns before submitting.

swift
let b = Arca.orderBreakdown(options: OrderBreakdownOptions(
amount: "200", amountType: .spend,
leverage: 10, feeRate: data.feeRate,
price: data.markPx, side: .buy, szDecimals: 5
))
// b.tokens — position size (committed quantity)
// b.notionalUsd — position exposure (~$1,991)
// b.marginRequired — margin from balance (~$199)
// b.estimatedFee — fee from balance (~$0.90)
// b.totalSpend — total from balance (~$200)

Timestamps in Market Data

Market data methods (getCandles, getSparklines, candle watch streams) use Unix epoch milliseconds (UTC) for all time values. This includes the startTime / endTime parameters on getCandles() and the t field on each Candle object.

Daily (.oneDay) candles open at UTC midnight boundaries. Use Int(Date().timeIntervalSince1970 * 1000) for the current time — no timezone conversion is needed.

Other API timestamps (operations, events, objects) use RFC 3339 UTC strings (e.g. 2026-03-31T14:30:00.000000Z). Only market data uses numeric epoch milliseconds.

Market Metadata

Global market data endpoints — not scoped to a specific exchange object. Call getMarketMeta() to discover supported assets and their constraints. Use this data for client-side order validation before submission.

getMarketMeta()

Returns the full perps universe with per-asset constraints.

swift
let meta = try await arca.getMarketMeta()
for asset in meta.universe {
print(asset.name, asset.maxLeverage, asset.szDecimals)
}

Each SimMetaAsset in universe contains:

nameString
Asset name (e.g. "BTC", "ETH").
szDecimalsInt
Number of decimal places for order size precision. For example, szDecimals: 4 means sizes must be multiples of 0.0001. Truncate or round order sizes to this precision before submitting.
maxLeverageInt
Maximum leverage multiplier allowed for this asset. Varies by asset (e.g. BTC may allow 40x while smaller assets allow 50x). The SDK rejects orders with leverage exceeding this value.
onlyIsolatedBool
If true, only isolated margin is supported — cross-margin is not available for this asset.
candleHistoryCandleHistoryBounds?
Candle history availability. Contains earliestMs (absolute earliest candle timestamp, including extended pre-listing data) and hlEarliestMs (earliest venue-native candle timestamp). When earliestMs < hlEarliestMs, extended history is available. Use earliestMs as startTime for a “Max” chart button.
displayNameString?
Human-readable name for the asset (e.g. "Bitcoin", "Crude Oil"). Use for UI labels; fall back to symbol when absent.
isHip3Bool?
Whether this asset is a HIP-3 deployer market.
deployerDisplayNameString?
Full display name of the HIP-3 deployer. Omitted for native perps.

Other Market Endpoints

swift
// Current mid prices
let mids = try await arca.getMarketMids()
// L2 order book
let book = try await arca.getOrderBook(coin: "hl:BTC")
// OHLCV candles
let candles = try await arca.getCandles(coin: "hl:BTC", interval: .oneMinute)
// candles.candles[0].t, .o, .h, .l, .c, .v, .n
// Sparklines — 24 hourly close prices for all coins in one request.
// Pre-computed every ~5 minutes in the background; all tracked coins are
// always included. For real-time price info, use mid prices or candle subscriptions.
let sparklines = try await arca.getSparklines()
// sparklines.sparklines["hl:BTC"] => [60100.5, 60200.1, ...] (24 hourly close prices)
// Newly listed coins may have fewer points until candle history accumulates.

Candle Subscriptions (WebSocket)

Subscribe to real-time candle updates for specified coins and intervals. Use the ref-counted acquireCandles / releaseCandles API to safely share subscriptions across components.

swift
arca.ws.acquireCandles(coins: ["hl:BTC", "hl:ETH"], intervals: [.oneMinute, .fiveMinutes])
for await event in await arca.ws.candleEvents() {
print(event.coin, event.interval, event.candle)
}
arca.ws.releaseCandles(coins: ["hl:BTC", "hl:ETH"], intervals: [.oneMinute, .fiveMinutes])

Real-time Streaming

The SDK provides high-level watch*() methods that manage the WebSocket connection, authentication, path-scoped watches, snapshots, and automatic reconnection. Each method returns a stream object with updates (an AsyncStream) and a stop() function for cleanup.

All watch*() methods work with both API keys and scoped JWT tokens. When using Arca(token:), the WebSocket authenticates with the same token and respects its scope.

watchBalances(arcaRef:)

Recommended for live balance display

Use watchBalances() for any view that shows account balances. It delivers an initial snapshot plus real-time updates. Use getBalancesByPath() only for one-shot reads.

Watch real-time balance updates, optionally filtered by path prefix. The server sends a full snapshot on watch, then pushes incremental updates.

swift
// Watch all balances
let stream = try await arca.watchBalances()
// Or filter by path prefix
let stream = try await arca.watchBalances(arcaRef: "/wallets/main")
// Initial snapshot is available immediately
let snapshot = stream.balances.value
// [String: BalanceSnapshot] — keyed by object ID
// React to updates via structured concurrency
for await (entityId, event) in stream.updates {
let path = event.entityPath ?? entityId
print("Balance changed on \(path)")
}
// Stop when the view disappears
await stream.stop()

BalanceSnapshot contains entityId, entityPath, and balances: [ArcaBalance].

watchOperations()

Subscribe to real-time operation creates and state changes. The operations property contains the running list, populated from the server's initial snapshot and updated as events arrive.

swift
let stream = try await arca.watchOperations()
print(stream.operations.value.count) // initial operations
for await (operation, event) in stream.updates {
guard operation.state.isTerminal else { continue }
if operation.state == .failed || operation.state == .expired {
print("Failed:", operation.failureMessage ?? "unknown")
} else {
print("Completed:", operation.type, operation.path)
}
}
await stream.stop()

Use this for ambient observation of all operation outcomes — for example, showing error toasts when transfers fail. Filter by sourceArcaPath / targetArcaPath to scope to the current user's objects.

getObjectValuation(path:)

Get the valuation for a single Arca object. Uses the same computation path as aggregation (Axiom 10: Observational Consistency).

swift
let valuation = try await arca.getObjectValuation(path: "/strategies/alpha")
print("Equity:", valuation.valueUsd)

watchObject(path:exchange:)

Subscribe to real-time valuation updates for a single Arca object. Returns an ObjectWatchStream that emits ObjectValuation updates on structural changes and mid price ticks. The SDK automatically watches mid prices and revalues positions client-side, so valueUsd, unrealizedPnl, and markPrice update in real time without consuming server bandwidth.

swift
let stream = try await arca.watchObject(path: "/strategies/alpha")
for await valuation in stream.updates {
print("Equity:", valuation.valueUsd)
print("Positions:", valuation.positions ?? [])
}
await stream.stop()

watchObjects(paths:exchange:)

Subscribe to real-time valuations for multiple Arca objects. Returns an ObjectsWatchStream that emits [String: ObjectValuation] on every update from any watched object. Optional exchange defaults to "sim". Call stop() to tear down all underlying watches.

swift
let stream = try await arca.watchObjects(paths: [
"/users/alice/main",
"/users/bob/main",
])
for await valuationsByPath in stream.updates {
for (path, valuation) in valuationsByPath {
print(path, valuation.valueUsd)
}
}
await stream.stop()

revalued(with:)

The ObjectValuation and PathAggregation types provide a revalued(with:) method for client-side recomputation using fresh mid prices. For PathAggregation, spot breakdown entries are revalued; exchange and perp entries pass through until the next server push; departingUsd and arrivingUsd are USD pass-through and unchanged. Used internally by watchObject, watchObjects, and watchAggregation, and available for manual use.

swift
let revalued = valuation.revalued(with: ["hl:BTC": "60000", "hl:ETH": "3000"])
let revaluedAgg = aggregation.revalued(with: mids)

watchCandleChart(coin:interval:count:)

Create a live candlestick chart with a single method call. The stream loads historical candles, then continuously merges live WebSocket events. The first update contains the full historical array, and every subsequent update includes the complete blended candle array — your app just renders update.candles. Reconnection gaps are filled automatically.

swift
let stream = try await arca.watchCandleChart(
coin: "hl:BTC",
interval: .oneMinute,
count: 300 // historical candles to load
)
for await update in stream.updates {
// update.candles — full sorted array (historical + live)
// update.latestCandle — the candle that triggered this update
renderChart(update.candles)
}
await stream.stop()

Loading a specific range

When the chart viewport changes (zoom, resize, jump to date), call stream.ensureRange(start, end) with the time range you need. The SDK tracks which ranges have already been fetched, loads only the gaps, coalesces overlapping calls, and merges everything into the sorted candle array.

swift
// Chart zoom-out — tell the SDK what range is now visible:
let result = await stream.ensureRange(newVisibleStart, newVisibleEnd)
// result.loadedCount == 0 means the range was already loaded, or an overlapping
// in-flight ensureRange finished covering it before this call completed.
// result.reachedStart == true means no more history exists

Loading older candles

For simple backward scrolling, stream.loadMore(count) fetches older candles before the current earliest. Returns a LoadRangeResult with the count of newly loaded candles and whether the start of history was reached.

swift
let result = await stream.loadMore(200)
if result.reachedStart {
// No more history available
}

watchCandles(coins:intervals:)

Low-level stream of raw candle events. Each event contains a single candle — your app must maintain the chart array manually. For candlestick charts, use watchCandleChart() above instead.

swift
let stream = try await arca.watchCandles(
coins: ["hl:BTC", "hl:ETH"],
intervals: [.oneMinute, .fiveMinutes]
)
for await event in stream.updates {
print(event.coin, event.interval, event.candle.close)
}
await stream.stop()

watchMaxOrderSize(options:)

Subscribe to a live, SDK-derived max order size stream for a specific coin and side. The SDK recomputes continuously from getExchangeState(), watchPrices(), and real-time exchange state updates for responsive UI sizing, while order submission remains backend-authoritative.

swift
let stream = try await arca.watchMaxOrderSize(options: MaxOrderSizeWatchOptions(
objectId: exchangeId,
coin: "hl:BTC",
side: .buy,
leverage: 5,
builderFeeBps: 40, // 4.0 bps (tenths of a bps)
szDecimals: 5
))
for await data in stream.updates {
print("max buy size", data.maxBuySize)
print("mark", data.markPx)
}
await stream.stop()

The stream updates when mid prices change and when exchange state changes (orders, fills, leverage, deposits, funding payments). The activeAssetData property holds the latest computed value.

Low-Level WebSocket API

For advanced use cases, the arca.ws manager provides direct access to path-scoped watches and typed event streams.

Connect & Watch

swift
// Connect and watch a path
await arca.ws.connect()
await arca.ws.watchPath("/")
// Iterate over all events
for await event in await arca.ws.events {
print(event.type, event.entityId ?? "")
}

Typed Event Streams

swift
// Operation events
for await (operation, event) in await arca.ws.operationEvents() {
print(operation.type, operation.state)
}
// Balance updates
for await (entityId, event) in await arca.ws.balanceEvents() {
print("Balance changed on", entityId)
}
// Exchange state updates
for await (state, event) in await arca.ws.exchangeEvents() {
print(state.marginSummary.equity)
}
// Mid price updates
for await mids in await arca.ws.midsEvents() {
print("BTC:", mids["hl:BTC"] ?? "n/a")
}
// Exchange fill events
for await (fill, event) in await arca.ws.fillEvents() {
print("Fill:", fill.coin, fill.size, "@", fill.price)
}

Connection Status

swift
for await status in await arca.ws.statusStream {
switch status {
case .connecting: print("Connecting...")
case .connected: print("Connected")
case .disconnected: print("Disconnected")
}
}

Path Watch Management

swift
// Watch specific paths (ref-counted)
await arca.ws.watchPath("/users/alice")
await arca.ws.unwatchPath("/users/alice")
// Mid price subscriptions (ref-counted)
arca.ws.acquireMids(exchange: "sim-hl")
arca.ws.releaseMids()
// Disconnect
await arca.ws.disconnect()

Path watches use prefix matching — watching /users/alice delivers events for all objects under that path. Watch / to receive all realm events. Watches are ref-counted; the server unwatch only fires when the last watcher releases.

Error Handling

The SDK uses a single ArcaError enum with associated values. All SDK methods are async throws.

swift
do {
try await arca.transfer(
path: "/op/transfer/fund-1",
from: "/wallets/alice",
to: "/wallets/bob",
amount: "250.00"
).settled
} catch let error as ArcaError {
switch error {
case .validation(let message, _):
print("Invalid input:", message)
case .notFound(_, let message, _):
print("Not found:", message)
case .conflict(_, let message, _):
print("Conflict:", message)
case .unauthorized(let message, _):
print("Auth failed:", message)
default:
print("Error:", error.localizedDescription)
}
}

Error Cases

CaseHTTP StatusWhen
.validation400Invalid input
.unauthorized401Missing or expired token
.forbidden403Insufficient permissions
.notFound404Resource not found
.conflict409Duplicate or invalid state
.internalError500Server error
.exchangeError502Exchange service error
.networkErrorNetwork failure
.decodingErrorResponse parsing failure

ArcaUI Module

The ArcaUI module provides SwiftUI components for common Arca interactions. It is distributed as a separate product in the same Swift package — add ArcaUI to your target dependencies alongside ArcaSDK.

Installation

swift
// Package.swift
targets: [
.target(name: "MyApp", dependencies: ["ArcaSDK", "ArcaUI"]),
]

PaymentLinkView

A SwiftUI view that wraps SFSafariViewController to present payment link pages in-app. The end user completes the deposit or withdrawal flow inside the Safari browser sheet without leaving your app.

swift
import ArcaUI
struct CheckoutView: View {
@State private var showPayment = false
let paymentURL: URL // from createPaymentLink().paymentLink.url
var body: some View {
Button("Pay Now") {
showPayment = true
}
.paymentLinkSheet(
isPresented: $showPayment,
url: paymentURL,
onDismiss: {
// Refresh state after payment
}
)
}
}

PaymentLinkView can also be used directly as a standalone view if you need more control over presentation:

swift
PaymentLinkView(url: paymentURL) {
// Called when the user dismisses the Safari view
print("Payment flow dismissed")
}

Note: ArcaUI requires iOS (UIKit) and is not available on macOS.