Kotlin SDK
The com.github.arcaresearch:arca-kotlin-sdk library is an idiomatic Kotlin client for the Arca platform — built for Android apps and any JVM 8+ runtime. Suspend functions model request/response, mutations return coroutine-based operation handles, real-time data is delivered through Kotlin Flow / StateFlow watch streams, and errors are a sealed ArcaException hierarchy. The library targets JVM 1.8 bytecode (Android minSdk 24+).
Installation
The SDK is distributed via JitPack. Add the JitPack repository, then the dependency:
// settings.gradle.ktsdependencyResolutionManagement { repositories { google() mavenCentral() maven { url = uri("https://jitpack.io") } }}
// app/build.gradle.ktsdependencies { implementation("com.github.arcaresearch:arca-kotlin-sdk:v0.1.0")}Pin a specific release with the version tag (e.g. v0.1.0). The published Maven artifact id is arca-sdk; the SDK pulls in OkHttp, kotlinx.coroutines, and kotlinx.serialization transitively.
Android setup. The SDK makes network calls, so add <uses-permission android:name="android.permission.INTERNET" /> to your manifest. All suspend functions are main-safe (OkHttp dispatches I/O off the main thread); call them from a coroutine scope such as viewModelScope or lifecycleScope.
Initialization
The Arca SDK is client-side: authenticate with a scoped JWT minted by your backend (never embed an API key in an app). Use the Arca(token = ...) factory, optionally supplying a tokenProvider for automatic refresh. The realm id is read from the token claims unless you pass realmId explicitly.
import network.arca.sdk.Arca
// Scoped JWT (realm read from the token claims)val arca = Arca(token = jwt)
// Auto-refreshing token provider — called now and again before expiry.val arca = Arca.withTokenProvider( tokenProvider = { fetchArcaTokenFromMyBackend() }, realmId = "my-realm",)
// Always release the WebSocket + coroutine scope when done (e.g. onCleared).arca.close()End-to-end example
val alice = "/users/alice/wallet"val bob = "/users/bob/wallet"
// Create two wallets. settled() suspends until the create operation is terminal.arca.ensureDenominatedArca(ref = alice).settled()arca.ensureDenominatedArca(ref = bob).settled()
// Fund Alice (dev/test only — use createPaymentLink in production).arca.fundAccount(arcaRef = alice, amount = "1000").settled()
// Transfer $50 — the nonce path is the idempotency key.val nonce = arca.nonce("/op/transfer/alice-to-bob/001")arca.transfer(path = nonce.path, from = alice, to = bob, amount = "50").settled()
val balances = arca.getBalancesByPath(bob)println("Bob settled: ${balances.first().settled}") // 50Conventions. Money/amount values are decimal strings ("50", "0.01"). Market ids are canonical {exchange}:{id} ("hl:0:BTC", "hl:1:TSLA"), case-sensitive. Market-data timestamps are Unix epoch milliseconds; all others are RFC 3339 UTC strings.
Arca Objects
Arca objects are the addressable accounts in a realm, identified by a human-readable path. ensureDenominatedArca and ensureArca are idempotent creators that return an OperationHandle; read methods are plain suspend functions.
// Idempotent create (denominated wallet).val created = arca.ensureDenominatedArca(ref = "/users/alice/wallet").settled()
// Any object type.arca.ensureArca(ref = "/exchanges/main", type = ArcaObjectType.EXCHANGE).settled()
// Reads.val obj = arca.getObject("/users/alice/wallet")val balances = arca.getBalances(obj.id.value)val byPath = arca.getBalancesByPath("/users/alice/wallet")
// Delete by path (optionally sweep remaining funds first).arca.ensureDeleted(ref = "/users/alice/wallet", sweepTo = "/treasury/main").settled()Transfers & Deposits
transfer moves value between two Arca paths; the operation path is the idempotency key. fundAccount / defundAccount are development-realm tools — for production deposits and withdrawals use a payment link.
val nonce = arca.nonce("/op/transfer/payroll/2026-06")arca.transfer( path = nonce.path, from = "/treasury/main", to = "/users/alice/wallet", amount = "250.00",).settled()
// Dev-only seeding.arca.fundAccount(arcaRef = "/users/alice/wallet", amount = "1000").settled()Payment Links
createPaymentLink returns an OperationHandle resolving to a hosted deposit/withdrawal URL for production money movement.
val link = arca.createPaymentLink( type = PaymentLinkType.DEPOSIT, arcaRef = "/users/alice/wallet", amount = "100",).settled()println(link.paymentLink.url)Operations & Events
Every mutation is an operation with a terminal state (completed, failed, expired). Fetch one by id, or page a realm’s event log.
val detail = arca.getOperation("op_...")println(detail.operation.state)
// Reserve an idempotency path (monotonic nonce under a prefix).val nonce = arca.nonce("/op/withdraw/alice")Aggregation & P&L
Aggregation rolls up valuations across a path prefix. Point-in-time reads are suspend functions; the live equity / P&L chart streams are covered under Real-time Streaming.
// Roll-up valuation for everything under a prefix.val agg = arca.getPathAggregation("/users/alice")println("Total equity: ${agg.totalEquityUsd}")
// Historical equity / P&L series for charts.val equity = arca.getEquityHistory(path = "/users/alice", from = fromIso, to = toIso)val pnl = arca.getPnlHistory(path = "/users/alice", from = fromIso, to = toIso)Exchange (Perps)
Place orders against an exchange-typed Arca object. placeOrder returns an OrderHandle: await settled() for placement confirmation, filled() for terminal fills, or collect fills(...) as a Flow.
val state = arca.getExchangeState(objectId)
val order = arca.placeOrder( path = arca.nonce("/op/order/btc-long").path, objectId = objectId, market = "hl:0:BTC", side = OrderSide.BUY, orderType = OrderType.LIMIT, size = "0.01", price = "50000", leverage = 5,)
// Wait for placement, then for the order to fully fill.order.settled()val filled = order.filled(timeoutSeconds = 30.0)println("Filled ${filled.fills.size} fill(s)")
// Or react to fills as they stream in.order.fills().collect { fill -> println("fill: ${fill.size} @ ${fill.price}") }Real-time Streaming
Watch factories open a shared WebSocket and return a stream object exposing a StateFlow for the latest snapshot plus a Flow of incremental updates. Call ready() to suspend until the first snapshot, and stop() when finished.
Snapshot-typed updates flows (prices, valuations, aggregation, exchange state, max order size, equity/P&L/candle charts) replay the latest snapshot to collectors that attach late and drop intermediate snapshots for slow collectors. Event-typed flows (operations, balances, fills, funding) do not replay — start collecting before you mutate to observe every event.
// Live valuation for one object.val watch = arca.watchObject("/users/alice/wallet")watch.ready()println(watch.valuation.value?.valueUsd)val job = scope.launch { watch.updates.collect { v -> render(v.valueUsd) }}
// Live mid prices for all markets.val prices = arca.watchPrices()println(prices.prices.value["hl:0:BTC"])
// Merge several objects into one keyed snapshot.val merged = arca.watchObjects(listOf("/users/alice/wallet", "/treasury/main"))
// Self-healing equity / P&L charts (history + live tail).val chart = arca.watchEquityChart(path = "/users/alice", from = fromIso, to = toIso)
// Always release.job.cancel()watch.stop()prices.stop()merged.stop()chart.stop()Lifecycle. Streams are reference-counted on the shared socket; the connection self-heals across reconnects and app background/foreground transitions. Each watch must be stop()-ed, and arca.close() tears down the socket and the SDK’s coroutine scope entirely.
Operation & Order Handles
Mutations return a handle backed by a coroutine Deferred, so the request is in-flight the moment you call the method. Await the stage you care about:
| Method | Resolves when |
|---|---|
submitted() | The server accepts the request (operation may be pending). |
settled() / settle() | The operation reaches a terminal state; throws ArcaException.OperationFailed on failed/expired. |
settled(timeoutSeconds) | As above, but throws ArcaException.Unknown("TIMEOUT") past the deadline. |
filled(timeoutSeconds) (OrderHandle) | The order is terminal with fills. |
fills(timeoutSeconds) (OrderHandle) | A Flow of fills as they arrive; closes on terminal status. |
// Fire-and-forget the request, await the HTTP accept only.val handle = arca.transfer(path = nonce.path, from = a, to = b, amount = "10")val accepted = handle.submitted()
// ...or block until value actually settles.val done = handle.settled()Error Handling
All failures are subclasses of the sealed ArcaException. Branch on the type with a when expression; HTTP-mapped cases carry a server code / errorId.
import network.arca.sdk.ArcaException
try { arca.transfer(path = nonce.path, from = a, to = b, amount = "10").settled()} catch (e: ArcaException) { when (e) { is ArcaException.Validation -> showFieldError(e.message) is ArcaException.Unauthorized -> reauthenticate() is ArcaException.Forbidden -> showPermissionError() is ArcaException.NotFound -> showMissing(e.code) is ArcaException.Conflict -> showConflict(e.code) is ArcaException.Exchange -> showExchangeError(e.code) is ArcaException.OperationFailed -> showFailed(e.operation.state) is ArcaException.Network -> retryLater() is ArcaException.Internal, is ArcaException.Decoding, is ArcaException.NonJsonResponse, is ArcaException.Unknown -> reportUnexpected(e) }}| Exception | Meaning |
|---|---|
Validation | 400 — invalid request input. |
Unauthorized | 401 — missing/expired token. |
Forbidden | 403 — insufficient permission. |
NotFound | 404 — resource/path does not exist (carries code). |
Conflict | 409 — idempotency/state conflict (carries code). |
Exchange | Venue-side rejection (carries code). |
OperationFailed | An operation reached failed/expired. |
Network | Transport failure (wraps the cause). |
Internal / Decoding / NonJsonResponse / Unknown | Server 5xx, unparseable body, or unclassified failure. |