Go SDK
The github.com/arcaresearch/arca-go-sdk module is an idiomatic Go client for the Arca platform — ideal for building your backend in Go. Every method takes a context.Context, errors are typed (usable with errors.As), mutations return generic operation handles, and real-time data is delivered via channel/callback watch streams. Requires Go 1.23+.
Installation
go get github.com/arcaresearch/arca-go-sdk@latestimport arca "github.com/arcaresearch/arca-go-sdk"It is a public module, so no GOPRIVATE or auth is needed. Pin a specific release with @vX.Y.Z (e.g. @v0.1.1).
Initialization
Create a client with an API key (server-side), a scoped JWT, or an auto-refreshing token provider, then call Ready to resolve the realm slug to its internal id (it also resolves lazily on the first call).
ctx := context.Background()
// API key (backend)client, err := arca.New(arca.Config{APIKey: "arca_...", Realm: "my-realm"})if err != nil { log.Fatal(err)}if err := client.Ready(ctx); err != nil { log.Fatal(err)}
// Scoped JWT (realm read from the token claims)client, _ = arca.FromToken(jwt, arca.Config{})
// Auto-refreshing token providerclient, _ = arca.FromTokenProvider(func(ctx context.Context) (string, error) { return fetchArcaTokenFromMyBackend(ctx)}, arca.Config{Realm: "my-realm"})End-to-end example
alice := "/users/alice/wallet"bob := "/users/bob/wallet"
// Create two wallets (Wait blocks until the create operation settles).if _, err := client.EnsureDenominatedArca(ctx, arca.EnsureDenominatedArcaOptions{Ref: alice}).Wait(ctx); err != nil { log.Fatal(err)}if _, err := client.EnsureDenominatedArca(ctx, arca.EnsureDenominatedArcaOptions{Ref: bob}).Wait(ctx); err != nil { log.Fatal(err)}
// Fund Alice (dev/test only — use CreatePaymentLink in production).if _, err := client.FundAccount(ctx, arca.FundAccountOptions{ArcaRef: alice, Amount: "1000"}).Wait(ctx); err != nil { log.Fatal(err)}
// Transfer $50 — the nonce path is the idempotency key.nonce, _ := client.Nonce(ctx, "/op/transfer/alice-to-bob/001")if _, err := client.Transfer(ctx, arca.TransferOptions{ Path: nonce.Path, From: alice, To: bob, Amount: "50",}).Wait(ctx); err != nil { log.Fatal(err)}
balances, _ := client.GetBalancesByPath(ctx, bob)fmt.Println("Bob settled:", balances[0].Settled) // 50Conventions. Money/amount values are decimal strings ("50", "0.01"). Coin ids are canonical {exchange}:{id} ("hl:0:BTC", "hl:1:TSLA"), case-sensitive. Market-data timestamps are Unix epoch milliseconds; all others are RFC3339 UTC strings.
Authentication
Three credential modes, mirroring the TypeScript SDK:
| Mode | Constructor | Use |
|---|---|---|
| API key | arca.New(Config{APIKey, Realm}) | Server-side backends |
| Scoped JWT | arca.FromToken(jwt, Config{}) | Per-user token; realm read from claims |
| Token provider | arca.FromTokenProvider(fn, Config{Realm}) | Auto-refresh before expiry & on 401 |
A token provider is called on first use, ~30s before expiry, and on HTTP 401 (then the failed request is retried once). Concurrent refreshes are deduplicated.
Step-up auth
Destructive actions on production realms return HTTP 412 STEP_UP_REQUIRED. Register a handler to obtain a single-use step-up token; the SDK retries the original request once with it and never persists it.
client, _ := arca.New(arca.Config{ APIKey: "arca_...", Realm: "prod", StepUpHandler: func(ctx context.Context, ch arca.StepUpChallenge) (string, error) { return confirmInBrowser(ctx, ch.Action, ch.Resources) // returns the step-up JWT },})Auth-error listener
unsub := client.OnAuthError(func(err error) { showSessionExpiredUI()})defer unsub()Arca Objects
Objects are accounts/wallets addressed by path. Creation is idempotent — safe to call on every request.
// Create a user wallet (denominated object)wallet, _ := client.EnsureDenominatedArca(ctx, arca.EnsureDenominatedArcaOptions{ Ref: "/users/alice/wallet", Labels: map[string]string{"tier": "gold"},}).Wait(ctx)
// Any object typeclient.EnsureArca(ctx, arca.EnsureArcaOptions{Ref: "/escrow/x", Type: arca.ObjectEscrow})
// Readobj, _ := client.GetObject(ctx, "/users/alice/wallet")detail, _ := client.GetObjectDetail(ctx, obj.ID)list, _ := client.ListObjects(ctx, &arca.ListObjectsOptions{Path: "/users/"})browse, _ := client.BrowseObjects(ctx, &arca.BrowseObjectsOptions{Path: "/users/"})
// Balancesbals, _ := client.GetBalances(ctx, obj.ID)bals, _ = client.GetBalancesByPath(ctx, "/users/alice/wallet")
// Labels + deleteclient.UpdateLabels(ctx, arca.UpdateLabelsOptions{ObjectID: obj.ID, Labels: map[string]*string{"tier": nil}})client.EnsureDeleted(ctx, arca.EnsureDeletedOptions{Ref: "/users/alice/wallet", SweepTo: "/treasury"}).Wait(ctx)Transfers & Deposits
The operation Path is the idempotency key — generate it once with Nonce and reuse it on retries.
nonce, _ := client.Nonce(ctx, "/op/transfer/payroll/001")resp, err := client.Transfer(ctx, arca.TransferOptions{ Path: nonce.Path, From: "/users/alice/wallet", To: "/users/bob/wallet", Amount: "50",}).Wait(ctx)
// Production deposits/withdrawals: hosted payment linkslink, _ := client.CreatePaymentLink(ctx, arca.CreatePaymentLinkOptions{ Type: "deposit", ArcaRef: "/users/alice/wallet", Amount: "100", ReturnURL: "https://myapp.com/done",}).Wait(ctx)// redirect the user to link.PaymentLink.URL
// Dev/test onlyclient.FundAccount(ctx, arca.FundAccountOptions{ArcaRef: "/users/alice/wallet", Amount: "1000"}).Wait(ctx)client.DefundAccount(ctx, arca.DefundAccountOptions{ArcaPath: "/users/alice/wallet", Amount: "10"}).Wait(ctx)
fee, _ := client.EstimateFee(ctx, arca.FeeEstimateParams{Action: "transfer", Amount: "50", SourcePath: "/a", TargetPath: "/b"})Operations & Events
Every state change is an operation with a lifecycle. Read them, the immutable event log, and balance deltas.
op, _ := client.GetOperation(ctx, "op_123", &arca.GetOperationOptions{IncludeEvidence: true})ops, _ := client.ListOperations(ctx, &arca.ListOperationsOptions{Type: arca.OpTransfer, Path: "/users/"})events, _ := client.ListEvents(ctx, &arca.ListEventsOptions{ArcaPath: "/users/alice/wallet"})deltas, _ := client.ListDeltas(ctx, "/users/alice/wallet")summary, _ := client.Summary(ctx)
// Block until a specific operation reaches a terminal state.final, err := client.WaitForOperation(ctx, "op_123", 30*time.Second)Exchange (Perps)
Create an exchange object, then place orders. PlaceOrder and ClosePosition return an *OrderHandle.
ex, _ := client.EnsurePerpsExchange(ctx, arca.CreatePerpsExchangeOptions{Ref: "/traders/t1/exchange"}).Wait(ctx)
nonce, _ := client.Nonce(ctx, "/op/order/btc")order := client.PlaceOrder(ctx, arca.PlaceOrderOptions{ Path: nonce.Path, ObjectID: ex.Object.ID, Coin: "hl:0:BTC", Side: arca.Buy, OrderType: "MARKET", Size: "0.01",})if _, err := order.Wait(ctx); err != nil { /* placement failed */ }fill, _ := order.Filled(ctx)
state, _ := client.GetExchangeState(ctx, ex.Object.ID)positions, _ := client.ListPositions(ctx, ex.Object.ID)client.UpdateLeverage(ctx, arca.UpdateLeverageOptions{ObjectID: ex.Object.ID, Coin: "hl:0:BTC", Leverage: 5})
// Margin mode: switch an asset between cross and isolated (close any open// position first; rejected on isolated-only HIP-3 markets).client.SetMarginMode(ctx, arca.SetMarginModeOptions{ObjectID: ex.Object.ID, Coin: "hl:0:BTC", MarginMode: arca.MarginModeIsolated})
// Isolated margin: add (+) or remove (-) collateral from an isolated position.client.UpdateIsolatedMargin(ctx, arca.UpdateIsolatedMarginOptions{ObjectID: ex.Object.ID, Coin: "hl:0:BTC", Amount: "25"})
client.ClosePosition(ctx, arca.ClosePositionOptions{Path: "/op/close/btc", ObjectID: ex.Object.ID, Coin: "hl:0:BTC"})Position TP/SL (ergonomic)
SetStopLoss / SetTakeProfit attach a trigger to an open position: they infer the closing side, by default place a reduce-only unsized (SizeToMax: true) order (when it fires it closes the entire live position regardless of size, and it is cancelled when the position closes), and auto-fill leverage + isolated. Set Size (base units) on SetPositionTriggerOptions to scale out a fixed quantity instead — the leg becomes a sized reduce-only partial (capped at the live position; min-notional waived). SetPositionTpsl brackets both legs at once and links them as a true one-cancels-the-other pair: it stamps both legs 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 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. SetStopLoss/SetTakeProfit return an *OrderHandle and error with *arca.NotFoundError when there is no open position.
// Stop-loss on a long BTC position (reduce-only SELL trigger at 54k)sl := client.SetStopLoss(ctx, arca.SetPositionTriggerOptions{ Path: "/op/tpsl/btc-sl", ObjectID: ex.Object.ID, Coin: "hl:0:BTC", TriggerPx: "54000",})if _, err := sl.Submitted(ctx); err != nil { /* no position, etc. */ }
// Bracket both legs at once (derives /op/tpsl/btc/sl and /op/tpsl/btc/tp)res, _ := client.SetPositionTpsl(ctx, arca.SetPositionTpslOptions{ Path: "/op/tpsl/btc", ObjectID: ex.Object.ID, Coin: "hl:0:BTC", StopLossPx: "54000", TakeProfitPx: "72000",})_ = res.StopLoss // *OrderHandle; res.TakeProfit likewise
// Clear resting position triggers (pass Tpsl: "sl"/"tp" to filter)cleared, _ := client.ClearPositionTpsl(ctx, arca.ClearPositionTpslOptions{ Path: "/op/tpsl/btc-clear", ObjectID: ex.Object.ID, Coin: "hl:0:BTC",})_ = cleared // []SimOrder that were cancelledOpen with bracket (atomic entry + TP/SL)
OpenWithBracket opens a position and attaches 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). The result holds 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.
res, err := client.OpenWithBracket(ctx, arca.OpenBracketOptions{ Path: "/op/bracket/btc-1", ObjectID: ex.Object.ID, Market: "hl:0:BTC", Side: arca.Buy, Size: "0.01", TakeProfitPx: "72000", StopLossPx: "58000",})if err != nil { /* validation / placement error */ }
if _, err := res.Entry.Wait(ctx); err != nil { /* ... */ }filled, _ := res.Entry.Filled(ctx) // entry fully fills_ = res.TakeProfit // *OrderHandle (nil if TakeProfitPx empty); res.StopLoss likewise_ = filled
// Scale out half at the target, then protect the remainder with an unsized// stop attached separately (the bracket OCO would otherwise cancel a shared-// group stop on the partial TP fill):so, _ := client.OpenWithBracket(ctx, arca.OpenBracketOptions{ Path: "/op/bracket/btc-scaleout", ObjectID: ex.Object.ID, Market: "hl:0:BTC", Side: arca.Buy, Size: "0.02", TakeProfitPx: "72000", TakeProfitSz: "0.01", // sized: close half at the target})if _, err := so.Entry.Filled(ctx); err == nil { client.SetStopLoss(ctx, arca.SetPositionTriggerOptions{ Path: "/op/bracket/btc-scaleout/sl", ObjectID: ex.Object.ID, Coin: "hl:0:BTC", TriggerPx: "58000", // unsized: protects the remainder })}Order breakdown (pure, no network)
bd := arca.ComputeOrderBreakdown(arca.OrderBreakdownOptions{ Amount: "200", AmountType: "spend", Leverage: 10, FeeRate: "0.00045", Price: "65000", Side: arca.Buy, SzDecimals: 5,})fmt.Println(bd.Tokens, bd.MarginRequired, bd.EstimatedFee, bd.EstimatedLiquidationPrice)Market data & TWAP
meta, _ := client.GetMarketMeta(ctx)mids, _ := client.GetMarketMids(ctx)book, _ := client.GetOrderBook(ctx, "hl:0:BTC")candles, _ := client.GetCandles(ctx, "hl:0:BTC", arca.Interval1h, nil)
client.PlaceTwap(ctx, arca.PlaceTwapOptions{ Path: nonce.Path, ExchangeID: ex.Object.ID, Coin: "hl:0:BTC", Side: arca.Buy, TotalSize: "1.0", DurationMinutes: 120,}).Wait(ctx)Real-time Streaming
Watch streams expose OnUpdate(cb) func(), an Updates() <-chan T channel, Ready(ctx), State(), and Close(). Open once and read on demand or react to every change.
// Prices — snapshot is pre-loaded; read any time.prices, _ := client.WatchPrices(ctx, &arca.WatchPricesOptions{Coins: []string{"hl:0:BTC"}})defer prices.Close()px, ok := prices.Get("hl:0:BTC")prices.OnUpdate(func(m map[string]string) { /* tick */ })
// Balances under a prefixbalStream, _ := client.WatchBalances(ctx, "/users")defer balStream.Close()for u := range balStream.Updates() { fmt.Println("balance changed:", u.EntityPath)}Also available: WatchOperations, WatchObject, WatchObjects, WatchAggregation, WatchExchangeState, WatchFills, WatchFunding, WatchCandles, WatchTrades, and WatchTwap. Lower-level access is on client.WS().
Operation & Order Handles
Mutations return a handle immediately; the HTTP request runs in the background.
Submitted(ctx)— the HTTP response, before settlement.Wait(ctx)— blocks until the operation reaches a terminal state.WaitTimeout(ctx, d)— Wait with an explicit settlement timeout.Predicted— synchronous optimistic effect (no network).
h := client.Transfer(ctx, opts)submitted, _ := h.Submitted(ctx) // acknowledged, not yet settledfinal, err := h.Wait(ctx) // settled (or *OperationFailedError / *OperationStalledError)*OrderHandle embeds the operation handle and adds the fill lifecycle:
order := client.PlaceOrder(ctx, opts)order.Wait(ctx) // order placedresult, _ := order.Filled(ctx) // wait for terminal fill stateunsub := order.OnFill(ctx, func(f arca.SimFill) { /* each fill */ })summary, _ := order.FillSummary(ctx) // platform-side P&L / fee breakdowncancel, _ := order.Cancel(ctx, "") // auto-derives the cancel path// Resize a sized order (limit / sized TP/SL). Unsized triggers are rejected.resize, _ := order.Resize(ctx, "0.75", "") // auto-derives a per-resize pathError Handling
Errors are typed and work with errors.As. The base type *arca.ArcaError carries Code, Message, and an optional ErrorID for support correlation.
_, err := client.GetObject(ctx, "/nope")
var notFound *arca.NotFoundErrorif errors.As(err, ¬Found) { /* 404 */ }
var ae *arca.ArcaErrorif errors.As(err, &ae) && ae.Code == "IDEMPOTENCY_VIOLATION" { /* same path, different inputs */ }
// Awaited operationsvar failed *arca.OperationFailedErrorif errors.As(err, &failed) { fmt.Println("failed:", failed.Operation.FailureMessage)}| Type | When |
|---|---|
*ValidationError | Invalid request (400) |
*UnauthorizedError | Missing/invalid auth (401) |
*NotFoundError | Resource not found (404) |
*ConflictError | Idempotency conflict (409) |
*ExchangeError | Exchange rejected the operation |
*OperationFailedError | Awaited operation reached a failed state |
*StepUpRequiredError | 412 with no step-up handler wired |
License
The Go SDK is licensed under the PolyForm Shield License 1.0.0 — source-available and free to use to build on Arca, but it may not be used to build a product that competes with Arca. The same applies to the TypeScript, React, and Swift SDKs.