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)

Manual: reconnect()

If your app uses custom lifecycle events (e.g. returning to foreground in a complex SwiftUI app) and the automatic WebSocket reconnect is not firing quickly enough, you can manually force a reconnect:

swift
await arca.reconnect()

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,
market: "hl:0: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?.direction ?? "", 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
// Resize a sized order (limit / sized TP/SL). Unsized triggers are rejected.
try await order.resize("0.75").settled

The order path is the idempotency key. Server-side wrappers should use a stable path per logical order attempt and return once the order has been submitted, rather than waiting inside an HTTP request for placement or fill settlement.

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)
.resize(_:path:)OperationHandleResize a sized order; unsized triggers rejected (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). Safe to call on every request. Denominated and exchange objects are USD-denominated; the field is server-stamped and not a request input.

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",
operationPath: "/op/create/wallets/main:1" // optional idempotency key
).settled
refStringrequired
Full Arca path.
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
).settled
refStringrequired
Full Arca path.
typeArcaObjectTyperequired
One of: .denominated, .exchange, .deposit, .withdrawal, .escrow.
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)
// Recovery-hatch state: nil for active boundaries, populated when the
// recovery-key holder has acted on-chain. UIs should disable mutating
// actions on the object when boundary != nil — the server returns
// 409 BOUNDARY_FROZEN otherwise.
if let boundary = obj.boundary {
print(boundary.status, boundary.recoveryArcaPath ?? "")
}
let list = try await arca.listObjects(prefix: "/wallets")
print(list.total)

Each ArcaObject may include an optional boundary: BoundarySnapshot? describing the recovery-hatch state of the object's isolation boundary. The field is nil for active boundaries (the happy path) and populated when the boundary is soft_frozen or hard_frozen. soft_frozen is reversible (recovery key holder may unlock); hard_frozen is terminal — the platform sweeps every wallet in the boundary into a system-owned recovery arca at recoveryArcaPath and refuses new operations on the boundary.

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.
feeOverrideString?
Override the transfer fee for this operation. Set to "0" to disable the fee. Non-production realms only.

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:

  • Development: 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, .swap, .order, .fill, .cancel, .feeDistribution, .adjustment, .funding, .venueClose, or .twap. Unknown future values decode as .unknown(String) so stream delivery can continue.
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

Server-Authoritative Pricing (forward-compatible)

The SDK advertises a server-authoritative-pricing capability on every request — an X-Arca-Client-Capabilities header on REST calls and a capabilities field in the WebSocket auth message. Valuation payloads — ObjectValuation, PathAggregation, and ExchangeState — carry an optional pricingMode field (.client or .server). When the server sends .server, the SDK trusts the server-computed USD values verbatim instead of recomputing them locally from raw mid prices (and watchMaxOrderSize sources its sizing from getActiveAssetData rather than the live mid). When pricingMode is absent or .client, valuation is byte-for-byte identical to prior SDK versions. This contract is behavior-neutral today — it exists so a future simulated-account price overlay can be enabled server-side without a client upgrade. No code changes are required to adopt it.

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 1000 points, max 1000). The backend ladder picks the finest resolution that fits inside points; the default targets 5m for 24h, 1h for 1M, and 4h for 3M ranges.

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 map historical points to their actual historical equityUsdand the live point to the current account equity. When set to .equity, each PnlPoint includes a valueUsd field suitable for the chart y-axis. This provides a true historical portfolio value view.
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)
// Per-position cost accumulators (running totals over the position's
// current open lot, reset on full close or flip-through-zero):
if let pos = state.positions.first {
print(pos.cumulativeFunding) // funding paid (-) or received (+) on this lot
print(pos.cumulativeFee) // total trading fee on this lot (sum of the three below)
print(pos.cumulativeExchangeFee) // exchange (taker / maker) component
print(pos.cumulativePlatformFee) // platform component
print(pos.cumulativeBuilderFee) // builder component
}

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,
market: "hl:0:BTC",
leverage: 10
)
// Get leverage for a coin
let setting = try await arca.getLeverage(objectId: exchangeId, market: "hl:0:BTC")
objectIdStringrequired
Exchange Arca object ID.
marketStringrequired
Market to set leverage for.
leverageIntrequired
Leverage multiplier (1 to maxLeverage).

The returned LeverageSetting includes marginMode(.cross or .isolated).

updateIsolatedMargin

Add or remove collateral from an isolated-margin position. A positiveamount (decimal USD string) moves balance into the position, lowering its liquidation price; a negative amount removes collateral, raising it. Removal is rejected if it would drop the position below its maintenance margin. Only valid on isolated positions.

swift
let result = try await arca.updateIsolatedMargin(
objectId: exchangeId,
market: "hl:1:CL",
amount: "25" // add $25; use a negative string to remove
)
print(result.isolatedMargin, result.liquidationPrice)
objectIdStringrequired
Exchange Arca object ID.
marketStringrequired
Market of the isolated position.
amountStringrequired
Signed decimal USD: positive adds, negative removes collateral.

setMarginMode

Switch an asset between cross and isolated margin. Rejected on isolated-only (HIP-3) markets and while an open position exists for the asset — close the position first. Leverage is remembered per mode, so switching restores the leverage last set for that mode.

swift
let result = try await arca.setMarginMode(
objectId: exchangeId,
market: "hl:0:BTC",
marginMode: .isolated
)
print(result.marginMode)
objectIdStringrequired
Exchange Arca object ID.
marketStringrequired
Market to switch margin mode for.
marginModeMarginModerequired
.cross or .isolated.

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. Reduce-only orders never reconfigure stored leverage but still carry leverage through to the venue so Hyperliquid can identify the isolated-margin bucket.

Isolated margin. Isolated-only markets — those whose marginModes is ["isolated"] (e.g. hl:1:CL) — reject cross orders at Hyperliquid's matching engine. Pass isolated: true with a positive leverage when opening or increasing on those markets; a reduceOnly close/trim is accepted without leverage (it uses the position's current leverage and never re-margins the remainder). Margin mode is independent of HIP-3 — some HIP-3 markets (e.g. hl:1:TSLA) are cross-eligible. closePosition() auto-fills both fields from the position and market metadata; you only need to set them yourself when calling placeOrder directly.

swift
let order = arca.placeOrder(
path: "/op/order/btc-buy-1",
objectId: exchangeId,
market: "hl:0: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)

The preferred way to close a position is closePosition() — it auto-fills leverage from the position and isolatedfrom the market's marginModes. A reduce-only close no longer requires leverage, but pre-filling it lets a partial close preserve the remainder's leverage. Pass isolated or leverage to override the inference.

swift
// Auto-fills leverage + isolated from the position and market meta
try await arca.closePosition(
path: "/op/close/btc-1",
objectId: exchangeId,
market: "hl:0:BTC"
).settled
// Partial close
try await arca.closePosition(
path: "/op/close/btc-partial",
objectId: exchangeId,
market: "hl:0:BTC",
size: "0.005"
).settled

For limit-price closes, call placeOrder directly with reduceOnly: true. Reduce-only orders skip the margin check and never reconfigure stored leverage.

swift
try await arca.placeOrder(
path: "/op/order/btc-close-1",
objectId: exchangeId,
market: "hl:0: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.
marketStringrequired
Canonical market ID to trade (e.g., hl:0:BTC, hl:0: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.
sizeToMaxBool?
Marks an unsized ("size to max") TP/SL: it carries no fixed quantity and closes the entire position when triggered, regardless of size. Leave nil/false for a sized TP/SL that closes its fixed size (reduce-only). Either way, no TP/SL outlives the position.

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

Position TP/SL (ergonomic)

Attach a stop-loss or take-profit to an existing position with setStopLoss / setTakeProfit. Each looks up the open position, infers the closing side (LONG → .sell, SHORT → .buy), and by default places a reduce-only unsized (sizeToMax: true) order, so when it fires it closes the entire live position regardless of size, and it is cancelled when the position closes or is liquidated. Pass a base-unit size to scale out only part of the position instead — the leg becomes a sized reduce-only partial (capped at the live position; min-notional waived). Leverage and isolated are auto-filled from the position and market meta. Both return an OrderHandle and throw ArcaError.notFound when there is no open position for the coin.

swift
// Stop-loss on a long position → reduce-only SELL trigger
try await arca.setStopLoss(
path: "/op/tpsl/btc-sl",
objectId: exchangeId,
market: "hl:0:BTC",
triggerPx: "54000"
).submitted
// Take-profit
try await arca.setTakeProfit(
path: "/op/tpsl/btc-tp",
objectId: exchangeId,
market: "hl:0:BTC",
triggerPx: "72000"
).submitted

A new trigger replaces any existing position TP/SL of the same type by default (replace: true); pass replace: false to stack. setPositionTpsl attaches both legs at once (at least one of stopLossPx / takeProfitPx required) and returns SetPositionTpslResult (stopLoss / takeProfit handles), deriving <path>/sl and <path>/tp. The two legs are linked as a true one-cancels-the-other bracket: both are stamped with one opaque ocoGroupId, so when either leg fills (even partially) the venue cancels the sibling with cancelReason == "sibling_filled" (readable on the cancelled order via getOrder/listOrders). Pass an explicit ocoGroupId to reuse a known group. Use stopLossSz / takeProfitSz (base units) for sized partial legs; because a partial fill cancels its OCO sibling, setPositionTpsl does not auto-link the legs when either is sized — so a half-size take-profit won't cancel the stop on the remainder. clearPositionTpsl cancels the resting position triggers for a coin (optionally one tpsl leg) and returns the cancelled orders.

swift
// Bracket a position with both legs
let result = try await arca.setPositionTpsl(
path: "/op/tpsl/btc",
objectId: exchangeId,
market: "hl:0:BTC",
stopLossPx: "54000",
takeProfitPx: "72000"
)
// Later: clear both legs (or pass tpsl: .stopLoss / .takeProfit for one)
let cleared = try await arca.clearPositionTpsl(
path: "/op/tpsl/btc-clear",
objectId: exchangeId,
market: "hl:0:BTC"
)
pathStringrequired
Operation path (idempotency key). For setPositionTpsl, legs derive /sl and /tp.
objectIdStringrequired
Exchange Arca object ID.
marketStringrequired
Market of the open position (e.g. hl:0:BTC).
triggerPxStringrequired
Trigger (mark) price (setStopLoss / setTakeProfit).
sizeString?
Sized partial close for setStopLoss / setTakeProfit (base units). Omit for the default whole-position close.
stopLossPx / takeProfitPxString?
Per-leg trigger prices for setPositionTpsl; at least one required.
stopLossSz / takeProfitSzString?
Per-leg sized partial close for setPositionTpsl (base units). When set, that leg is a sized reduce-only partial and the legs are not auto-OCO-linked.
isMarketBool?
Execute as market on trigger (default true). When false, limitPrice is required.
replaceBool
Replace an existing same-type position trigger before placing (default true).
leverageInt?
Override the inferred leverage (defaults to the position's).
isolatedBool?
Override the inferred isolated flag (defaults to the market's margin mode).
tpslTpslType?
clearPositionTpsl only — restrict to .stopLoss or .takeProfit. Omit to clear both.

openWithBracket

Open a position and attach its reduce-only TP/SL triggers in one atomic batch (Hyperliquid normalTpsl parity). Unlike chaining placeOrder then setPositionTpsl, the entry and its triggers are submitted as a single signed batch to one operation: the whole bracket validates and commits at the venue, or none of it does. The trigger legs arm only when the entry fills (they can't fire on mark price before the position exists), and the venue links them with a shared one-cancels-the-other group. At least one of takeProfitPx / stopLossPx is required. Each trigger leg closes the whole position by default; set takeProfitSz / stopLossSz (base units) to make it a sized reduce-only partial (size is the entry size, not the trigger size). Returns OpenBracketResult with one OrderHandle per leg (entry, takeProfit?, stopLoss?), all backed by the single bracket operation. Because the legs share one server-stamped OCO group, a partial take-profit fill cancels the stop on the remainder — to “scale out half and keep a stop on the rest”, open the bracket with only the sized takeProfitSz and attach the (unsized) stop separately via setStopLoss. Use setPositionTpsl instead to attach TP/SL to an already-open position.

swift
let bracket = try arca.openWithBracket(
path: "/op/bracket/btc-1",
objectId: exchangeId,
market: "hl:0:BTC",
side: .buy,
size: "0.01",
takeProfitPx: "72000",
stopLossPx: "58000"
)
try await bracket.entry.settle() // bracket placed
_ = try await bracket.entry.filled() // wait for the entry to fully fill
// bracket.takeProfit / bracket.stopLoss arm automatically once the entry fills
pathStringrequired
Operation path (idempotency key) — owns the whole bracket.
objectIdStringrequired
Exchange Arca object ID.
marketStringrequired
Canonical market (e.g. hl:0:BTC).
sideOrderSiderequired
Entry side (.buy / .sell).
sizeStringrequired
Entry size in base-asset units.
orderTypeOrderType
Entry type (default .market). .limit requires price.
takeProfitPx / stopLossPxString?
Trigger (mark) prices; at least one required.
takeProfitSz / stopLossSzString?
Sized partial close for the matching trigger leg (base units). Omit for the default whole-position close.
triggersAreMarketBool
Fire TP/SL as market orders when triggered (default true).
groupingString
Batch grouping (default normalTpsl).

listOrders / getOrder / cancelOrder / modifyOrder

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
// Resize a resting order. Only sized orders can be resized (limit orders and
// sized TP/SL triggers); unsized (sizeToMax: true) triggers are rejected.
try await arca.modifyOrder(
path: "/op/modify/btc-1-0.75",
objectId: exchangeId,
orderId: orderId,
newSize: "0.75"
).settled

placeTwap / cancelTwap / getTwap / listTwaps / watchTwap

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. cancelTwap is idempotent for terminal-state TWAPs (already completed, cancelled, or failed): it returns the existing record with no error rather than 4xx.

swift
// Start a TWAP
let twap = try await arca.placeTwap(
exchangeId: exchangeId,
path: "/wallets/main/twap/btc-accumulate",
market: "hl:0:BTC",
side: .buy,
totalSize: "10.0",
durationMinutes: 120,
intervalSeconds: 30
)
print(twap.twap.status) // .active
// Cancel (idempotent for terminal-state TWAPs)
try await arca.cancelTwap(exchangeId: exchangeId, operationId: twap.operationId)
// Get status (now includes expectedSliceCount, targetPrice, failureReason)
let status = try await arca.getTwap(exchangeId: exchangeId, operationId: twap.operationId)
print(status.twap.executedSize, status.twap.filledSlices)
print(status.twap.expectedSliceCount, status.twap.targetPrice ?? "n/a")
// Live stream a single TWAP — handles initial REST fetch + WebSocket updates
for await twap in arca.watchTwap(exchangeId: exchangeId, operationId: twap.operationId) {
print("\(twap.sliceCount)/\(twap.expectedSliceCount ?? 0)", twap.status)
if twap.status != .active { break }
}
// List active TWAPs
let active = try await arca.listTwaps(exchangeId: exchangeId, activeOnly: true)
// Get limits + duration-keyed recommendation curve (cached after first call)
let limits = try await arca.getTwapLimits()
let interval = try await arca.recommendedIntervalSeconds(for: 60) // 5m for hour-long TWAPs

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.market, 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, market: "hl:0: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, market: "hl:0:BTC", leverage: 10
)
marketString
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 application fee.
maintenanceMarginRateString
Base maintenance margin rate as a decimal (e.g. "0.03" for 3%).
bidPxString?
Top-of-book best bid as a decimal string. Market sells are margin-checked at the bid, so this is the directional execution price for max-sell sizing. Equals markPx when no order book is available.
askPxString?
Top-of-book best ask as a decimal string. Market buys are margin-checked at the ask, so this is the directional execution price for max-buy sizing. Equals markPx when no order book is available.

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. When you also want a liquidation estimate, pass both maintenanceMarginRate andaccountContext: the helper computes a cross-margin estimate using the same formula the backend uses, and merges any existing same-coin position the same way it would after a fill.

swift
// Without liquidation estimate
let preview = Arca.orderBreakdown(options: OrderBreakdownOptions(
amount: "200", amountType: .spend,
leverage: 10, feeRate: data.feeRate,
price: data.markPx, side: .buy, szDecimals: 5
))
// With cross-margin liquidation estimate
let state = try await arca.getExchangeState(exchangeId)
let mmr = Double(data.maintenanceMarginRate) ?? 0
let otherMM = (state.positions ?? [])
.filter { $0.market != "hl:0:BTC" }
.reduce(0.0) { sum, p in
let sz = Double(p.size) ?? 0
let entry = Double(p.entryPrice) ?? 0
return sum + mmr * sz * entry
}
let existing = (state.positions ?? []).first { $0.market == "hl:0:BTC" }
let b = Arca.orderBreakdown(options: OrderBreakdownOptions(
amount: "200", amountType: .spend,
leverage: 10, feeRate: data.feeRate,
price: data.markPx, side: .buy, szDecimals: 5,
maintenanceMarginRate: data.maintenanceMarginRate,
accountContext: OrderBreakdownAccountContext(
equity: state.marginSummary.equity,
otherMaintenanceMargin: String(otherMM),
existingPosition: existing.map {
OrderBreakdownExistingPosition(side: $0.side, size: $0.size, entryPrice: $0.entryPrice)
}
)
))
// b.tokens, b.notionalUsd, b.marginRequired, b.estimatedFee, b.totalSpend
// b.estimatedLiquidationPrice — cross-margin estimate for the post-fill position

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() as the public asset catalog to discover supported assets, canonical IDs, category labels, icon metadata, and trading constraints. Assets listed by Hyperliquid are returned even when Arca has not curated a display name or logo yet.

getMarketMeta()

Returns the full perps universe with per-asset constraints.

Market IDs are case-sensitive. Use asset.name exactly as returned for orders, leverage, active asset data, candles, order books, and price streams. Use asset.symbol, asset.venueSymbol, and asset.displayName only for display or venue links. For example, Shiba Inu is "hl:0:kSHIB", not "hl:0:KSHIB".

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

Each Market in universe contains:

nameString
Canonical market identifier to pass back to trading and market-data methods exactly as returned. IDs are case-sensitive (e.g. "hl:0:BTC", "hl:0:kSHIB", or "hl:1:TSLA" for HIP-3 assets).
symbolString
Base asset symbol without exchange prefix (e.g. "BTC", "kSHIB"). Display-only — do not reconstruct API market IDs from this field.
venueSymbolString?
Venue-native market symbol for display or venue deep links (e.g. "BTC", "xyz:TSLA"). Do not pass this to Arca APIs; use name.
exchangeString
Exchange identifier (e.g. "hl" for Hyperliquid).
dexString?
HIP-3 DEX name, if the asset belongs to a builder-deployed DEX. Omitted for native Hyperliquid perps.
assetTypeString?
Machine-readable category when Arca recognizes the underlying instrument, e.g. crypto, equity, commodity, index, forex, etf, hl-native, or stablecoin. Omitted for newly listed venue assets that have not been mapped yet.
categoryLabelString?
Human-readable label derived from assetType, suitable for UI grouping.
mappedBool?
Whether Arca recognizes the underlying instrument in its asset registry. false means the asset is live on Hyperliquid but category metadata is not available yet.
hasDisplayNameBool?
Whether a curated displayName is available.
hasLogoBool?
Whether a curated logoUrl or logoSources entry is available.
descriptionStatusString?
curated when Arca has display metadata, otherwise hl_only for assets known only from the live venue listing.
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.
marginModes[String]?
The margin modes this asset supports: ["isolated"] for isolated-only markets, ["cross", "isolated"] otherwise. Read this instead of inferring from isHip3 — margin mode is independent of HIP-3 (some HIP-3 markets, e.g. hl:1:TSLA, are cross-eligible). Orders that open or increase on isolated-only markets must include isolated: true with a positive leverage; cross orders are rejected with a 400. A reduceOnly close is accepted without leverage. closePosition() auto-fills both.
onlyIsolatedBool
Deprecated — Hyperliquid-specific. Read marginModes instead. true is equivalent to marginModes being ["isolated"] (isolated-only). Removed in a future major version.
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.
logoUrlString?
Default CDN URL for the asset logo image (128px WebP). Use for asset icons in UI.
logoSources[LogoSource]?
Array of logo variants. Each source has a url, format, and width.
isHip3Bool?
Whether this asset is a HIP-3 deployer market.
deployerDisplayNameString?
Full display name of the HIP-3 deployer. Omitted for native perps.
feeScaleDouble?
HIP-3 fee multiplier. 1 for standard perps; greater than 1 for builder-deployed perps where the deployer set a deployerFeeScale.

market(_:)

Look up a single market by its canonical id. This is an exact-id lookup — pass the name field of a Market (e.g. "hl:0:BTC"), not a bare symbol like "BTC". To go from a human symbol to its market(s), use resolveMarkets(_:) below. Lazily fetches and caches market metadata on first call — subsequent lookups return instantly from cache without a network request.

swift
let btc = try await arca.market("hl:0:BTC")
print(btc?.symbol) // "BTC"
print(btc?.displayName) // nil or "Bitcoin"
print(btc?.logoUrl) // "https://...-128.webp" (default 128px)
print(btc?.logoSources?.first?.width) // 256
print(btc?.maxLeverage) // 50
print(btc?.szDecimals) // 5
// HIP-3 markets work the same way
let tsla = try await arca.market("hl:1:TSLA")
print(tsla?.displayName) // "Tesla"
print(tsla?.isHip3) // true
print(tsla?.venueSymbol) // "xyz:TSLA"
print(tsla?.deployerDisplayName) // "xyz"
// Returns nil for an unknown id — and for a bare symbol, which is
// never a canonical id (use resolveMarkets for symbols).
let nope = try await arca.market("hl:0:DOESNOTEXIST") // nil
let alsoNil = try await arca.market("BTC") // nil

resolveMarkets(_:exchange:dex:) / resolveMarketOrThrow(_:exchange:dex:)

Go from a human symbol ("BTC", "TSLA") to the market(s) that carry it. A single symbol can legitimately map to many markets — across exchanges and across HIP-3 dexes — so resolveMarkets returns an array. An empty array is an explicit “no market has this symbol”, never a silent guess. Matching is exact and case-sensitive on Market.symbol ("kSHIB""KSHIB"); narrow ambiguous symbols with the optional exchange / dex arguments.

Use resolveMarketOrThrow for the “I expect exactly one” case — it throws when zero markets match (so a typo never silently no-ops) or when more than one matches (so an ambiguous symbol never silently resolves to the wrong market). If you already hold a canonical id, use market(_:) instead.

swift
// One symbol can map to many markets — resolveMarkets returns them all.
let all = try await arca.resolveMarkets("BTC") // every market whose symbol is "BTC"
// Narrow with exchange / dex when a symbol is ambiguous.
// (dex is the HIP-3 deployer DEX name; native Hyperliquid perps omit it.)
let btc = try await arca.resolveMarketOrThrow("BTC", exchange: "hl")
try await arca.placeOrder(
path: "/op/order/btc-buy-1",
objectId: exchangeId,
market: btc.name, // pass the canonical id, e.g. "hl:0:BTC"
side: .buy,
orderType: .market,
size: "0.01"
).settled
// HIP-3 markets are namespaced by their deployer DEX:
let tsla = try await arca.resolveMarketOrThrow("TSLA", dex: "xyz")
// No match is an explicit empty array, not a guess:
let none = try await arca.resolveMarkets("NOTATICKER") // []

preloadMarketMeta() / refreshMarketMeta()

Call preloadMarketMeta() at app startup to warm the cache eagerly and avoid latency on the first market(_:) call. Call refreshMarketMeta() to force re-fetch metadata (e.g. after a new asset is listed).

swift
// Warm the cache at startup
try await arca.preloadMarketMeta()
// Force re-fetch when metadata may have changed
try await arca.refreshMarketMeta()

Other Market Endpoints

swift
// Current mid prices
let mids = try await arca.getMarketMids()
// L2 order book
let book = try await arca.getOrderBook(market: "hl:0:BTC")
// OHLCV candles
let candles = try await arca.getCandles(market: "hl:0: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:0: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:0:BTC", "hl:0:ETH"], intervals: [.oneMinute, .fiveMinutes])
for await event in await arca.ws.candleEvents() {
print(event.market, event.interval, event.candle)
}
arca.ws.releaseCandles(coins: ["hl:0:BTC", "hl:0: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:0:BTC": "60000", "hl:0:ETH": "3000"])
let revaluedAgg = aggregation.revalued(with: mids)

watchCandleChart(market: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(
market: "hl:0: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:0:BTC", "hl:0:ETH"],
intervals: [.oneMinute, .fiveMinutes]
)
for await event in stream.updates {
print(event.market, 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. A single getActiveAssetData fetch at setup resolves the per-asset maintenanceMarginRate (used to populate ActiveAssetData.maintenanceMarginRate for orderBreakdown's liquidation estimate), the asset's laddered marginTiers, and the top-of-book bidPx/askPx. Market buys are margin-checked at the ask and sells at the bid, so the stream sizes against that directional price (carried forward as a stable spread ratio applied to the live mid) and against the tiered initial-margin schedule — so the previewed max matches what the venue will accept for tiered or wide-spread assets like BTC. Pass maintenanceMarginRate explicitly to override the fetched value.

maxBuyUsd/maxSellUsd are a live estimate, not a guarantee

The streamed max accounts for fees, leverage, tiered initial margin, the bid/ask spread, and existing positions, plus a small (~10 bps) safety buffer. Because mids and the spread move between preview and submission, the executable max at the venue can still drift by a few basis points. To place exactly the maximum without risking an insufficient balance rejection, submit with useMax: true (the server sizes to the live max atomically) or pass a small sizeTolerance so the server shrinks the order to fit rather than rejecting it.

swift
let stream = try await arca.watchMaxOrderSize(options: MaxOrderSizeWatchOptions(
objectId: exchangeId,
market: "hl:0:BTC",
side: .buy,
leverage: 5,
applicationFeeTenthsBps: 40, // 4.0 bps (tenths of a bps)
szDecimals: 5
// maintenanceMarginRate: omit to auto-fetch (one extra REST call), or pass
// explicitly (e.g. "0.01" for BTC) to skip the lookup
))
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:0:BTC"] ?? "n/a")
}
// Exchange fill events
for await (fill, event) in await arca.ws.fillEvents() {
print("Fill:", fill.market, 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

Logging & diagnostics

The SDK emits structured diagnostic records for network calls, WebSocket lifecycle events, watch streams, candle chart updates, and token refresh paths. Records flow to two destinations:

  • Apple unified logging (os.Logger) under subsystem io.arcaos.sdk. Works in Xcode and Console.app with no configuration.
  • A host-provided ArcaLogHandler for forwarding records to Datadog, Sentry, Crashlytics, or a custom backend.

Configuration

Pass logLevel and an optional logHandler when constructing Arca. The default level is .warning — quiet in production, but enough to surface failures that were previously silent.

swift
#if DEBUG
let arca = try Arca(token: jwt, logLevel: .debug)
#else
let arca = try Arca(token: jwt, logLevel: .warning)
#endif

Record shape

Records delivered to your handler carry a level, a category, the message, an optional underlying error, and structured metadata like path, market, httpMethod, statusCode, and errorId.

swift
public enum ArcaLogLevel: Int, Comparable, Sendable {
case debug, info, notice, warning, error
}
public struct ArcaLogRecord: Sendable {
public let level: ArcaLogLevel
public let category: String // "network" | "websocket" | "watch" | "cdn" | "auth" | "candle"
public let message: String
public let error: Error?
public let metadata: [String: String]
public let timestamp: Date
}
public protocol ArcaLogHandler: Sendable {
func handle(_ record: ArcaLogRecord)
}

Xcode / Console.app

Records are written to the unified logging system under subsystem io.arcaos.sdk. In Console.app, filter by subsystem. From the terminal:

bash
log stream --level debug --predicate 'subsystem == "io.arcaos.sdk"'

Datadog adapter

swift
import DatadogLogs
struct DatadogArcaLogger: ArcaLogHandler {
let logger: DatadogLogger
func handle(_ record: ArcaLogRecord) {
var attrs: [String: Encodable] = record.metadata
attrs["category"] = record.category
switch record.level {
case .error: logger.error(record.message, error: record.error, attributes: attrs)
case .warning: logger.warn(record.message, error: record.error, attributes: attrs)
case .notice, .info: logger.info(record.message, attributes: attrs)
case .debug: logger.debug(record.message, attributes: attrs)
}
}
}
let arca = try Arca(
token: jwt,
logLevel: .warning,
logHandler: DatadogArcaLogger(logger: myDatadogLogger)
)

Sentry adapter

swift
import Sentry
struct SentryArcaLogger: ArcaLogHandler {
func handle(_ record: ArcaLogRecord) {
let breadcrumb = Breadcrumb(level: record.level.sentryLevel,
category: "arca.\(record.category)")
breadcrumb.message = record.message
breadcrumb.data = record.metadata
SentrySDK.addBreadcrumb(breadcrumb)
if record.level >= .error, let err = record.error {
SentrySDK.capture(error: err) { scope in
scope.setContext(value: record.metadata, key: "arca")
}
}
}
}
private extension ArcaLogLevel {
var sentryLevel: SentryLevel {
switch self {
case .debug: return .debug
case .info, .notice: return .info
case .warning: return .warning
case .error: return .error
}
}
}

Crashlytics adapter

swift
import FirebaseCrashlytics
struct CrashlyticsArcaLogger: ArcaLogHandler {
func handle(_ record: ArcaLogRecord) {
let meta = record.metadata
.map { "\($0)=\($1)" }
.sorted()
.joined(separator: " ")
Crashlytics.crashlytics().log("[\(record.category)] \(record.message) \(meta)")
if record.level >= .error, let err = record.error {
Crashlytics.crashlytics().record(error: err)
}
}
}

Custom backend

Handlers run on a serial queue, so you don't have to synchronize. Forward records anywhere — an HTTP endpoint, a file, a third-party analytics service — by implementing the single handle(_:) method.

swift
struct StderrArcaLogger: ArcaLogHandler {
func handle(_ record: ArcaLogRecord) {
let line = "[\(record.category)/\(record.level)] \(record.message)\n"
FileHandle.standardError.write(Data(line.utf8))
}
}

Categories & levels

CategoryCovers
networkREST requests, retries, response decoding
websocketConnect, disconnect, reconnect backoff, server errors, delivery gaps, stale detection
authToken refresh attempts, 401 handling, proactive refresh failures
watchInitial snapshots, gap-recovery refetches, watch-stream errors that used to be swallowed
candleCandle chart initial load, retry loops, gap recovery, array-shrink guards
cdnCandle CDN chunk fetches and API fallbacks

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.