Arca/Documentation
Join Waitlist

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:

kotlin
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
// app/build.gradle.kts
dependencies {
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.

kotlin
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

kotlin
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}") // 50

Conventions. 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.

kotlin
// 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.

kotlin
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()

createPaymentLink returns an OperationHandle resolving to a hosted deposit/withdrawal URL for production money movement.

kotlin
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.

kotlin
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.

kotlin
// 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.

kotlin
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.

kotlin
// 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:

MethodResolves 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.
kotlin
// 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.

kotlin
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)
}
}
ExceptionMeaning
Validation400 — invalid request input.
Unauthorized401 — missing/expired token.
Forbidden403 — insufficient permission.
NotFound404 — resource/path does not exist (carries code).
Conflict409 — idempotency/state conflict (carries code).
ExchangeVenue-side rejection (carries code).
OperationFailedAn operation reached failed/expired.
NetworkTransport failure (wraps the cause).
Internal / Decoding / NonJsonResponse / UnknownServer 5xx, unparseable body, or unclassified failure.