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:
// Package.swiftdependencies: [ .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.
import ArcaSDK
// Recommended: with automatic token refreshlet 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.
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.
let id = await arca.onAuthError { error in showSessionExpiredUI()}
// Remove the listener when no longer neededawait 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:
// Custom cache sizelet arca = try Arca(token: scopedJwt, cache: CacheConfig(maxEntries: 100))
// Disable caching entirelylet 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
// Await full settlement in one linetry await arca.fundAccount(arcaRef: "/wallets/main", amount: "1000").settledProgressive Disclosure
let handle = arca.fundAccount(arcaRef: "/wallets/main", amount: "1000")
// Get the HTTP response immediately (operation may still be pending)let response = try await handle.submittedprint(response.poolAddress ?? "no pool")
// Wait for full settlementtry await handle.settled
// Or with an explicit timeouttry await handle.settled(timeoutSeconds: 15)Batching with async let
async let d1 = arca.fundAccount(arcaRef: "/wallets/main", amount: "500").settledasync let d2 = arca.fundAccount(arcaRef: "/wallets/savings", amount: "300").settledlet (r1, r2) = try await (d1, d2)OrderHandle
placeOrder returns an OrderHandle with additional exchange lifecycle methods for fills and cancellation:
let order = arca.placeOrder( path: "/op/order/btc-1", objectId: exchangeId, coin: "hl:BTC", side: .buy, orderType: .market, size: "0.01")
// Wait for placementtry await order.settled
// Wait for filllet filled = try await order.filled(timeoutSeconds: 30)print(filled.order.avgFillPrice ?? "")
// Stream fills as they arrivefor 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 fillslet unsub = order.onFill { fill in print("Got fill: \(fill.size)")}// later: unsub()
// Cancel the ordertry await order.cancel().settledAPI Summary
| Property / Method | Returns | Description |
|---|---|---|
.submitted | Response | HTTP response before settlement |
.settled | Response | Wait for full settlement |
.settled(timeoutSeconds:) | Response | Wait with explicit timeout |
.filled(timeoutSeconds:) | SimOrderWithFills | Wait 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) | () -> Void | Callback per fill, returns unsub closure (OrderHandle only) |
.cancel(path:) | OperationHandle | Cancel 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).
try await arca.ensureDenominatedArca( ref: "/wallets/main", denomination: "USD", operationPath: "/op/create/wallets/main:1" // optional idempotency key).settledrefStringrequireddenominationStringrequiredUSD, BTC).metadataString?operationPathString?ensureArca
Ensure an Arca object of any type exists at the given path. Creates if missing, returns existing if matching. Returns an OperationHandle.
try await arca.ensureArca( ref: "/deposits/d1", type: .deposit, denomination: "USD").settledrefStringrequiredtypeArcaObjectTyperequired.denominated, .exchange, .deposit, .withdrawal, .escrow.denominationString?.denominated type.metadataString?operationPathString?getObject / listObjects
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.
let detail = try await arca.getObjectDetail(objectId: obj.id.rawValue)print(detail.operations.count)print(detail.balances)print(detail.reservedBalances)getBalances / getBalancesByPath
let balances = try await arca.getBalances(objectId: obj.id.rawValue)for balance in balances { print(balance.denomination, balance.amount)}
// Or by pathlet balances = try await arca.getBalancesByPath(path: "/wallets/main")browseObjects
S3-style hierarchical browsing. Returns folders (path prefixes) and objects at the given level.
let result = try await arca.browseObjects(prefix: "/users/")getObjectVersions
Get all versions of an Arca object at the same path (for deleted + recreated objects).
let versions = try await arca.getObjectVersions(objectId: obj.id.rawValue)getSnapshotBalances
Get historical balances and positions for an object at a specific timestamp.
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.
// Simple deletiontry await arca.ensureDeleted(ref: "/wallets/old").settled
// Deletion with balance sweeptry await arca.ensureDeleted( ref: "/wallets/old", sweepTo: "/wallets/main").settled
// Exchange deletion with position liquidationtry await arca.ensureDeleted( ref: "/exchanges/hl1", sweepTo: "/wallets/main", liquidatePositions: true).settledrefStringrequiredsweepToString?liquidatePositionsBoolTransfers & Fund Account
transfer
Execute a transfer between two Arca objects. Returns an OperationHandle. The operation path serves as the idempotency key.
let result = try await arca.transfer( path: "/op/transfer/alice-to-bob-1", from: "/wallets/alice", to: "/wallets/bob", amount: "250.00").settledprint(result.operation.state) // .completedpathStringrequiredfromStringrequiredtoStringrequiredamountStringrequiredfundAccount
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.
// Wait for full settlementtry await arca.fundAccount(arcaRef: "/wallets/main", amount: "1000").settled
// Or get the HTTP response first, then waitlet handle = arca.fundAccount(arcaRef: "/wallets/main", amount: "1000")let response = try await handle.submittedprint(response.poolAddress ?? "simulated")try await handle.settledarcaRefStringrequiredamountStringrequireddefundAccount
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().
try await arca.defundAccount( arcaPath: "/wallets/main", amount: "500.00", destinationAddress: "0xabc...").settledarcaPathStringrequiredamountStringrequireddestinationAddressString?Payment Links
Create shareable URLs for deposits and withdrawals. End users can complete the payment without authentication — the link token serves as the credential.
createPaymentLink
Create a payment link for a deposit or withdrawal. Returns an OperationHandle.
let response = try await arca.createPaymentLink( type: .deposit, arcaRef: "/wallets/main", amount: "100.00", returnUrl: "https://example.com/thanks", expiresInMinutes: 60).submitted // .submitted for the link URL; .settled to wait for paymentprint(response.paymentLink.url) // shareable linkprint(response.paymentLink.status) // "pending"typePaymentLinkTyperequired.deposit or .withdrawal.arcaRefStringrequiredamountStringrequiredreturnUrlString?expiresInMinutesInt?metadata[String: Any]?listPaymentLinks
List payment links in the realm, optionally filtered by type and status.
let response = try await arca.listPaymentLinks(type: .deposit, status: "pending")for link in response.paymentLinks { print(link.id, link.status, link.amount, link.denomination)}typePaymentLinkType?statusString?"pending", "completed", "expired".Operations & Events
getOperation
Get operation detail by ID, including correlated events and state deltas.
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.
// Single typelet list = try await arca.listOperations(type: .transfer)
// Multiple types with inline contextlet history = try await arca.listOperations( types: [.fill, .transfer], includeContext: true)typeOperationType?.transfer, .create, .delete, .deposit, .withdrawal, .order, .cancel.types[OperationType]?type.includeContextBoolDisplay Helpers
Convenience methods for rendering operation history in strategy UIs.
// Transfer direction relative to an objectlet dir = op.transferDirection(for: "/exchanges/strat-1") // .incoming or .outgoing
// Friendly counterparty labellet label = op.counterpartyLabel(for: "/exchanges/strat-1") // "Vault"
// Context accessorslet amount = op.context?.transferAmount // "5000"let fee = op.context?.transferFee // "0.05"let denom = op.context?.transferDenomination // "USD"listEvents / getEventDetail
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).
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.
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").settledsummary
Get aggregate counts for the realm.
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.
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).
// Aggregate all objects under a prefixlet agg = try await arca.getPathAggregation(prefix: "/users/alice/")print(agg.totalEquityUsd, agg.breakdown.count)
// Single objectlet single = try await arca.getPathAggregation(prefix: "/users/alice/exchanges/hl1")
// Historical aggregation at a past timestamplet hist = try await arca.getPathAggregation( prefix: "/users/", asOf: "2026-02-15T00:00:00Z")Returns a PathAggregation:
prefixStringtotalEquityUsdStringdepartingUsdStringarrivingUsdString?breakdown[AssetBreakdown]asOfString?cumInflowsUsdString?flowsSince time (only present when a watch was created with flowsSince).cumOutflowsUsdString?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.
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:
prefixStringfromStringtoStringstartingEquityUsdStringendingEquityUsdStringnetInflowsUsdStringnetOutflowsUsdStringpnlUsdStringendingEquity - startingEquity - netInflows + netOutflows.externalFlows[ExternalFlowEntry]getEquityHistory
Get equity time-series sampled evenly over a time range (default 200 points, max 1000).
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:
prefixStringfromStringtoStringpointsIntequityPoints[EquityPoint]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.
// Track all objects under a user prefixlet watch = try await arca.createAggregationWatch(sources: [ AggregationSource(type: .prefix, value: "/users/alice/"),])print(watch.aggregation.totalEquityUsd)
// Combine multiple source typeslet 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.
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 timelet 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 doneawait 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.
// 1. Create the watchlet watch = try await arca.createAggregationWatch(sources: [ AggregationSource(type: .prefix, value: "/users/alice/"),])var portfolio = watch.aggregation
// 2. Listen for structural updateslet aggStream = await arca.ws.aggregationEvents()Task { for await (_, agg, _) in aggStream { if let agg = agg { portfolio = agg } }}
// 3. Revalue on mid price tickslet prices = try await arca.watchPrices()for await mids in prices.updates { portfolio = portfolio.revalued(with: mids) renderPortfolio(portfolio)}getWatchAggregation / destroyAggregationWatch
// Get current aggregation on demandlet 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.// 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.
try await arca.ensurePerpsExchange(ref: "/exchanges/hl1").settledrefStringrequiredoperationPathString?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.
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.
// Set leveragelet result = try await arca.updateLeverage( objectId: exchangeId, coin: "hl:BTC", leverage: 10)
// Get leverage for a coinlet setting = try await arca.getLeverage(objectId: exchangeId, coin: "hl:BTC")objectIdStringrequiredcoinStringrequiredleverageIntrequiredplaceOrder
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.
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 placementlet filled = try await order.filled(timeoutSeconds: 30) // wait for fillfor try await fill in order.fills() { /* ... */ } // stream fillstry await order.cancel().settled // cancelClose Position (Market, close-only)
For exits, set reduceOnly: true and omit leverage. Close-only paths should not trigger leverage changes.
try await arca.placeOrder( path: "/op/order/btc-close-1", objectId: exchangeId, coin: "hl:BTC", side: .sell, orderType: .market, size: "0.01", reduceOnly: true).settledReverse 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.
pathStringrequiredobjectIdStringrequiredcoinStringrequiredBTC, ETH).sideOrderSiderequired.buy or .sell.orderTypeOrderTyperequired.market or .limit.sizeStringrequiredpriceString?.limit orders.leverageInt?reduceOnlyBool?isTriggerBool?triggerPx and tpsl.triggerPxString?isMarketBool?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
// List orderslet orders = try await arca.listOrders(objectId: exchangeId)
// Get a specific order with fill historylet 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).settledplaceTwap / 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.
// Start a TWAPlet 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
// Canceltry await arca.cancelTwap(exchangeId: exchangeId, operationId: twap.operationId)
// Get statuslet status = try await arca.getTwap(exchangeId: exchangeId, operationId: twap.operationId)print(status.twap.executedSize, status.twap.filledSlices)
// List active TWAPslet active = try await arca.listTwaps(exchangeId: exchangeId, activeOnly: true)
// Get limits (local, no network)let limits = arca.twapLimitslistPositions
Each position includes returnOnEquity (unrealized P&L / margin used) and positionValue (size × mark price), computed server-side.
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.
let data = try await arca.getActiveAssetData(objectId: exchangeId, coin: "hl:BTC")// Use maxBuySize / maxSellSize for order size sliderslet maxLong = Double(data.maxBuySize) ?? 0let maxShort = Double(data.maxSellSize) ?? 0
// Pass leverage to override the stored settinglet data10x = try await arca.getActiveAssetData( objectId: exchangeId, coin: "hl:BTC", leverage: 10)coinStringleverageLeverageInfotype is .cross or .isolated.maxBuySizeStringmaxSellSizeStringmaxBuyUsdStringmaxSellUsdStringavailableToTradeStringmaxBuyUsd/maxSellUsd.markPxStringfeeRateString"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.
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.
let meta = try await arca.getMarketMeta()for asset in meta.universe { print(asset.name, asset.maxLeverage, asset.szDecimals)}Each SimMetaAsset in universe contains:
nameString"BTC", "ETH").szDecimalsIntszDecimals: 4 means sizes must be multiples of 0.0001. Truncate or round order sizes to this precision before submitting.maxLeverageIntonlyIsolatedBooltrue, only isolated margin is supported — cross-margin is not available for this asset.candleHistoryCandleHistoryBounds?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?"Bitcoin", "Crude Oil"). Use for UI labels; fall back to symbol when absent.isHip3Bool?deployerDisplayNameString?Other Market Endpoints
// Current mid priceslet mids = try await arca.getMarketMids()
// L2 order booklet book = try await arca.getOrderBook(coin: "hl:BTC")
// OHLCV candleslet 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.
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:)
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.
// Watch all balanceslet stream = try await arca.watchBalances()
// Or filter by path prefixlet stream = try await arca.watchBalances(arcaRef: "/wallets/main")
// Initial snapshot is available immediatelylet snapshot = stream.balances.value// [String: BalanceSnapshot] — keyed by object ID
// React to updates via structured concurrencyfor await (entityId, event) in stream.updates { let path = event.entityPath ?? entityId print("Balance changed on \(path)")}
// Stop when the view disappearsawait 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.
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).
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.
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.
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.
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.
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.
// 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 existsLoading 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.
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.
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.
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
// Connect and watch a pathawait arca.ws.connect()await arca.ws.watchPath("/")
// Iterate over all eventsfor await event in await arca.ws.events { print(event.type, event.entityId ?? "")}Typed Event Streams
// Operation eventsfor await (operation, event) in await arca.ws.operationEvents() { print(operation.type, operation.state)}
// Balance updatesfor await (entityId, event) in await arca.ws.balanceEvents() { print("Balance changed on", entityId)}
// Exchange state updatesfor await (state, event) in await arca.ws.exchangeEvents() { print(state.marginSummary.equity)}
// Mid price updatesfor await mids in await arca.ws.midsEvents() { print("BTC:", mids["hl:BTC"] ?? "n/a")}
// Exchange fill eventsfor await (fill, event) in await arca.ws.fillEvents() { print("Fill:", fill.coin, fill.size, "@", fill.price)}Connection Status
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
// 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()
// Disconnectawait 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.
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
| Case | HTTP Status | When |
|---|---|---|
.validation | 400 | Invalid input |
.unauthorized | 401 | Missing or expired token |
.forbidden | 403 | Insufficient permissions |
.notFound | 404 | Resource not found |
.conflict | 409 | Duplicate or invalid state |
.internalError | 500 | Server error |
.exchangeError | 502 | Exchange service error |
.networkError | — | Network failure |
.decodingError | — | Response 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
// Package.swifttargets: [ .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.
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:
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.