SDK: Installation & Setup
The @arcaresearch/sdk package provides a high-level, typed, idempotent interface for building on Arca. It requires Node.js 18+ and is written in TypeScript (ES2022).
Installation
npm install @arcaresearch/sdkPackage Exports
The package exports one main class and all supporting types:
import { Arca } from '@arcaresearch/sdk';
// Types are also exported for strong typingimport type { ArcaObject, ArcaBalance, Operation, TransferOptions, PlaceOrderOptions, RealmEvent, // ... 70+ exported types} from '@arcaresearch/sdk';Initialization
The SDK supports two authentication modes. In both cases, call arca.ready() to resolve the realm before making API calls (it is also called automatically on the first API call).
// API Key — for backend services, scripts, CIconst arca = new Arca({ apiKey: 'arca_78ae7276_...', realm: 'development', // slug or UUID baseUrl: 'http://localhost:3052', // optional});await arca.ready();
// Scoped Token — for end-user frontendsconst arca = Arca.fromToken(scopedJwt);// realm is extracted from the token claims automaticallyawait arca.ready();SDK: Authentication
The SDK mirrors the platform's dual-auth model. Choose the right mode for your context:
API Key (Backend)
API keys are team-scoped with full access across all realms. Use this for server-side code, scripts, and CI pipelines.
import { Arca } from '@arcaresearch/sdk';
const arca = new Arca({ apiKey: 'arca_78ae7276_...', realm: 'development',});apiKeystringrequiredrealmstringrequireddevelopment) or UUID.baseUrlstringhttps://api.arcaos.io.Scoped Token (Frontend)
Scoped tokens are short-lived JWTs minted by the builder's backend via POST /auth/token. They restrict access to specific paths and permissions within a single realm.
// With automatic refresh (recommended)const arca = Arca.fromToken(scopedJwt, { tokenProvider: async () => { const res = await fetch('/api/arca-token'); return (await res.json()).token; },});await arca.ready();
// Static token (manual refresh via updateToken())const arca = Arca.fromToken(scopedJwt);await arca.ready();If the token does not embed a realmId claim, provide it explicitly:
const arca = Arca.fromToken(scopedJwt, { realm: 'development' });tokenstringPOST /auth/token. Optional when tokenProvider is set.tokenProvider() => Promise<string>realmstringrealmId claim.baseUrlstringhttps://api.arcaos.io.Realm Resolution
The SDK accepts realm slugs (e.g., development) and resolves them to UUIDs on arca.ready(). If the input is already a UUID, no resolution call is made. For scoped tokens, the realmId claim is extracted from the JWT payload directly.
Choosing the Right Auth Mode
The recommended pattern: API key in your backend for writes, read-only scoped token in the frontend for reads and streaming. This gives your frontend real-time data without any write risk, while your backend retains full control over mutations.
| Context | Auth mode | Scope |
|---|---|---|
| Backend service, CI, scripts | new Arca({ apiKey } ) | Full access — all reads and writes |
| End-user frontend (default) | Arca.fromToken(jwt) | arca:Read — reads + streaming, no writes |
| Trading frontend (advanced) | Arca.fromToken(jwt) | arca:Read + arca:PlaceOrder + arca:CancelOrder |
See Architecture Patterns for a full risk-based decision guide on which write actions to expose to frontends.
Token Refresh
Scoped tokens are short-lived (default 60 min). The SDK provides two approaches to handle expiry, from fully automatic to manual:
Recommended: Token Provider (automatic)
Pass a tokenProvider function 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
// Recommended: pass a token provider for automatic refreshconst arca = new Arca({ token: initialScopedJwt, tokenProvider: async () => { const res = await fetch('/api/arca-token'); return (await res.json()).token; },});
// Or use the factory method (provider-only, no initial token)const arca = Arca.fromTokenProvider(async () => { const res = await fetch('/api/arca-token'); return (await res.json()).token;});
// Listen for unrecoverable auth failures (e.g. provider throws)arca.onAuthError((error) => { showSessionExpiredUI();});Manual: updateToken()
If you prefer full control, use updateToken() to swap the token yourself. The new token takes effect immediately for HTTP; if the WebSocket is disconnected, it reconnects immediately with the new token.
arca.updateToken(freshScopedJwt);Auth Error Event
Register an onAuthError listener to handle unrecoverable authentication failures centrally, regardless of which approach you use. This fires when a 401 cannot be recovered (no provider, or the provider itself fails).
const unsub = arca.onAuthError((error) => { console.error('Session expired:', error.message); redirectToLogin();});
// Call unsub() to remove the listenerHistory 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() and watchPnlChart() 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 config option:
// Custom cache sizeconst arca = new Arca({ apiKey: 'arca_78ae7276_...', realm: 'development', cache: { maxEntries: 100 },});
// Disable caching entirelyconst arca = new Arca({ apiKey: 'arca_78ae7276_...', realm: 'development', cache: false,});
// Manually clear the cache (e.g. after a known data change)arca.clearHistoryCache();Minting Tokens (ArcaAdmin)
The ArcaAdmin class provides a mintToken() method for your backend to create scoped JWTs:
import { ArcaAdmin } from '@arcaresearch/sdk';
const admin = new ArcaAdmin({ baseUrl: 'http://localhost:3052' });const { token: jwt } = await admin.signIn('you@company.com', 'password');admin.setToken(jwt);
// Mint a read-only token for an end-userconst { token: scopedJwt } = await admin.mintToken({ realmId: '6d25623e-...', sub: 'alice', scope: { statements: [ { effect: 'Allow', actions: ['arca:Read'], resources: ['*'] }, ], }, expirationMinutes: 30,});
// Return scopedJwt to the frontendSDK: Operation Handles
Every mutation method — ensureDenominatedArca, fundAccount, transfer, placeOrder, etc. — returns an OperationHandle<T> instead of Promise<T>. The handle is thenable: await waits for both HTTP submission and operation settlement, so the common case is a single line.
Simple: Await to Settlement
// Resolves when the fund operation has fully settledawait arca.fundAccount({ arcaRef: '/wallets/main', amount: '1000' });Progressive Disclosure
When you need the HTTP response before settlement (e.g., to show a pool address to the user), access handle.submitted:
const handle = arca.fundAccount({ arcaRef: '/wallets/main', amount: '1000' });
// Get the HTTP response immediately (before settlement)const { poolAddress, operation } = await handle.submitted;console.log(poolAddress); // show to user while settlement is in progress
// Wait for settlement with an explicit timeoutawait handle.wait({ timeout: 15000 });Batching
Handles work with Promise.all for parallel operations:
await Promise.all([ arca.fundAccount({ arcaRef: '/wallets/main', amount: '500' }), arca.fundAccount({ arcaRef: '/wallets/savings', amount: '300' }),]);OrderHandle (Exchange Orders)
placeOrder() returns an OrderHandle that extends OperationHandle with fill lifecycle methods:
const order = arca.placeOrder({ path: '/op/order/btc-buy-1', objectId: exchangeId, coin: 'hl:BTC', side: 'BUY', orderType: 'MARKET', size: '0.01',});await order; // wait for placement
// Wait for fillsconst filled = await order.filled({ timeout: 30000 });console.log(filled.fills);
// Stream fills via async iteratorfor await (const fill of order.fills()) { console.log(`Filled ${fill.size} @ ${fill.price}`);}
// Get structured fill summary (P&L, fee breakdown, direction, resulting position)const summary = await order.fillSummary({ timeout: 30000 });console.log(summary?.realizedPnl, summary?.dir, summary?.feeBreakdown);
// Callback-based fill listenerconst unsub = order.onFill((fill) => console.log(fill));
// Cancel the order (auto-generates cancel path from placement path)const cancelResult = await order.cancel();API Summary
| Property / Method | Available On | Description |
|---|---|---|
await handle | OperationHandle | Wait for full settlement |
handle.submitted | OperationHandle | Promise for the HTTP response (before settlement) |
handle.wait({ timeout }) | OperationHandle | Wait for settlement with explicit timeout |
order.filled({ timeout }) | OrderHandle | Wait until order is fully filled |
order.fillSummary({ timeout }) | OrderHandle | Get fill with P&L, fee breakdown, direction, and resulting position |
order.fills() | OrderHandle | Async iterator yielding fills as they arrive |
order.onFill(cb) | OrderHandle | Callback for each fill; returns unsubscribe |
order.cancel() | OrderHandle | Cancel the order |
SDK: Arca Objects
ensureDenominatedArca(opts)
Ensure a denominated Arca object exists at the given path. Creates it if it doesn't exist; returns the existing one if it does (matching type and denomination). Safe to call on every request — eliminates the need for separate create-then-get patterns. Returns an OperationHandle — await resolves when the object is active (immediate for existing objects, waits for creation workflow otherwise).
const { object } = await arca.ensureDenominatedArca({ ref: '/wallets/main', denomination: 'USD', operationPath: '/op/create/wallets/main:1', // optional idempotency key});refstringrequireddenominationstringrequiredUSD, BTC).metadatastringoperationPathstringensureArca(opts)
Ensure an Arca object of any type exists at the given path. Creates if missing, returns existing if matching. Returns an OperationHandle.
New-object creation is asynchronous. The immediate create response gives you a stable object ID right away, but object.createdAt and object.updatedAt may be empty until the background workflow persists the row. If you need an authoritative recency signal shared across devices, wait until the object appears in listObjects() or browseObjects().
const { object } = await arca.ensureArca({ ref: '/deposits/d1', type: 'deposit', denomination: 'USD',});refstringrequiredtypestringrequireddenominated, exchange, deposit, withdrawal, escrow.denominationstringdenominated type.metadatastringoperationPathstringgetObject(path)
Get the active Arca object at a given path.
const obj: ArcaObject = await arca.getObject('/wallets/main');console.log(obj.id, obj.status, obj.denomination);getObjectDetail(objectId)
Get full object detail including operations, events, state deltas, and balances.
const detail = await arca.getObjectDetail(obj.id);console.log(detail.operations.length); // all operations on this objectconsole.log(detail.balances); // current balancesconsole.log(detail.reservedBalances); // in-flight reserved amountsconsole.log(detail.object.isolation); // boundary metadata, when applicablelistObjects(opts?)
List Arca objects in the realm, optionally filtered by path prefix.
Persisted objects returned here always include authoritative createdAt and updatedAt timestamps. When you need deterministic newest-first selection across devices, sort siblings by createdAt descending and use a stable tie-breaker such as id.
const { objects, total } = await arca.listObjects({ prefix: '/wallets', includeDeleted: false, // default});getBalances(objectId)
Get current balances for an Arca object. Each balance includes four fields describing where value is during the transfer lifecycle:
settledstringarrivingstring"0.00" for denominated-to-denominated transfers.departingstringtotalstringarriving + settled + departing. The complete value attributed to this object.const balances: ArcaBalance[] = await arca.getBalances(obj.id);// [{ id: '...', arcaId: '...', denomination: 'USD', amount: '1000.00',// arriving: '200.00', settled: '800.00', departing: '0.00', total: '1000.00' }]
// Show in-flight state to usersfor (const b of balances) { if (b.arriving !== '0.00') console.log(`${b.denomination}: ${b.arriving} arriving`); if (b.departing !== '0.00') console.log(`${b.denomination}: ${b.departing} departing`); console.log(`${b.denomination}: ${b.settled} available, ${b.total} total`);}getBalancesByPath(path)
Get balances by Arca path (resolves path to ID internally).
const balances = await arca.getBalancesByPath('/wallets/main');browseObjects(opts?)
S3-style hierarchical browsing. Returns folders, richer path entries, and objects at the given level.
Like listObjects(), browsed objects have authoritative timestamps once persisted. This is the shared-data view to use when a client must choose the newest sibling account after a reset without any device-local memory.
Use paths and currentIsolation to render explorer-style isolation markers, including empty zones that exist before any object is created under them.
const { folders, paths, currentIsolation, objects } = await arca.browseObjects({ prefix: '/users/', includeDeleted: false,});
for (const entry of paths ?? []) { console.log(entry.path, entry.kind, entry.isolation?.isBoundaryRoot);}createIsolationZone(opts)
Declare a path as an isolation-zone root before creating any object beneath it. The call is idempotent by path and returns the existing zone if the same path was already declared.
const zone = await arca.createIsolationZone({ path: '/users/alice',});
console.log(zone.boundaryId);console.log(zone.isolation?.isBoundaryRoot); // truegetObjectVersions(objectId)
Get all versions of an Arca object at the same path (for deleted + recreated objects).
const { versions } = await arca.getObjectVersions(obj.id);getSnapshotBalances(objectId, asOf)
Get historical balances and positions for an object at a specific timestamp.
const snapshot = await arca.getSnapshotBalances(obj.id, '2026-02-18T18:00:00Z');console.log(snapshot.balances, snapshot.positions);ensureDeleted(opts)
Delete an Arca object by path. Returns an OperationHandle — await waits for deletion to complete. 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. Deletion is blocked if the object has in-flight operations. Deleted objects keep their canonical path, but the SDK also surfaces a tombstone displayName so repeated deleted generations remain distinguishable in explorer-style UIs.
Full withdrawals are different from deletes: when a non-exchange object reaches zero through withdrawal, the returned ArcaObject will eventually move tostatus: 'withdrawn' with withdrawnAt set. Withdrawn paths stay visible for historical reads and aggregation history, but they are permanently retired and cannot be reused for a new generation.
// Simple deletion (no balance)await arca.ensureDeleted({ ref: '/wallets/old' });
// Deletion with balance sweepawait arca.ensureDeleted({ ref: '/wallets/old', sweepTo: '/wallets/main',});
// Exchange deletion with position liquidationawait arca.ensureDeleted({ ref: '/exchanges/hl1', sweepTo: '/wallets/main', liquidatePositions: true,});refstringrequiredsweepTostringupdateLabels(objectId, labels)
Update labels on an Arca object. Uses merge semantics: keys with string values are set, keys with null values are removed. Existing labels not mentioned in the call are left unchanged.
const updated = await arca.updateLabels(obj.id, { displayName: 'Main Wallet', tier: 'gold', removeMe: null, // removes this key});console.log(updated.labels); // { displayName: 'Main Wallet', tier: 'gold' }objectIdstringrequiredlabelsRecord<string, string | null>required., _, -, and be 1–63 characters. Values are max 256 characters. Set a key to null to remove it. Max 32 keys per object, 4KB total size. Keys prefixed with arca. or _ are reserved.listObjects with labels
Pass include: 'labels' to include labels on each object in the list response.
const { objects } = await arca.listObjects({ prefix: '/wallets', include: 'labels',});for (const obj of objects) { if (obj.status === 'withdrawn') { console.log('retired at', obj.withdrawnAt); } console.log(obj.displayName ?? obj.labels?.displayName ?? obj.path);}SDK: Transfers & Fund Account
transfer(opts)
Execute a transfer between two Arca objects. Returns an OperationHandle. The operation path serves as the idempotency key — resubmitting the same path returns the existing result. Settlement is immediate for denominated targets or async for exchange targets; await handles both transparently.
const { operation, fee } = await arca.transfer({ path: '/op/transfer/alice-to-bob-1', from: '/wallets/alice', to: '/wallets/bob', amount: '250.00',});
console.log(operation.state); // 'completed'console.log(fee); // { amount: '0.05', denomination: 'USD' } (when applicable)pathstringrequiredfromstringrequiredtostringrequiredamountstringrequiredestimateFee(params)
Estimate the fee for a transfer or order operation before executing it.
const estimate = await arca.estimateFee({ action: 'transfer', amount: '1000.00', sourcePath: '/wallets/main', targetPath: '/exchange/main',});
console.log(estimate.fee); // { amount: '0.05', denomination: 'USD' }console.log(estimate.netAmount); // '999.95'actionstringrequiredtransfer or order.amountstringrequiredsourcePathstringtargetPathstringStatic Fee Constants
The Arca.fees static property provides known fee constants for client-side calculations without a network call.
console.log(Arca.fees.exchangeTransfer); // '0.05'fundAccount(opts)
Fund an Arca object (denominated or exchange). Returns an OperationHandle — await waits for full settlement. Throws OperationFailedError on failure. For exchange objects, the deposit is forwarded to the venue after on-chain confirmation. This is a developer tool for testing and account seeding — for production deposit flows, use createPaymentLink().
Settlement by Realm Type
fundAccount behavior depends on the realm type:
- demo / development / testing — auto-mints tokens to the realm's custody pool and settles automatically within seconds. No real tokens or wallet needed.
- production — creates a deposit intent and returns a
poolAddress. The operation stays pending until real tokens (USDC) are sent to that address on-chain. If no tokens arrive before the intent expires (30 min), the operation remains pending indefinitely. Usehandle.submittedto access thepoolAddressbefore settlement.
// await waits for full settlementawait arca.fundAccount({ arcaRef: '/wallets/main', amount: '1000.00',});
// Or access the HTTP response before settlementconst handle = arca.fundAccount({ arcaRef: '/wallets/main', amount: '1000.00' });const { poolAddress } = await handle.submitted;await handle.wait({ timeout: 15000 });arcaRefstringrequiredamountstringrequireddefundAccount(opts)
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().
await arca.defundAccount({ arcaPath: '/wallets/main', amount: '500.00', destinationAddress: '0xabc...',});arcaPathstringrequiredamountstringrequireddestinationAddressstringSDK: Payment Links
Create shareable URLs for deposits and withdrawals. End users can complete the payment without authentication — the link token serves as the credential.
createPaymentLink(opts)
Create a payment link for a deposit or withdrawal. Returns an OperationHandle. Use handle.submitted to get the link URL immediately, or await to wait for the payment to complete.
const handle = arca.createPaymentLink({ type: 'deposit', arcaRef: '/wallets/main', amount: '100.00', returnUrl: 'https://example.com/thanks', returnStrategy: 'redirect', expiresInMinutes: 60,});
// Get the link immediately (before the end user pays)const { paymentLink } = await handle.submitted;console.log(paymentLink.url); // shareable link for the end user
// Optionally wait for the payment to settleawait handle;type'deposit' | 'withdrawal'requiredarcaRefstringrequiredamountstringrequiredreturnUrlstring"redirect" and "navigate" strategies. Supports HTTP(S) and custom URL schemes (e.g., myapp://callback).returnStrategy'redirect' | 'close' | 'navigate'"redirect" (default) auto-redirects to an HTTP(S) returnUrl."navigate" uses window.location.href for custom URL schemes."close" calls window.close() to dismiss the web view (for native apps using SFSafariViewController or Android Custom Tabs).expiresInMinutesnumbermetadataRecord<string, unknown>listPaymentLinks(opts?)
List payment links in the realm, optionally filtered by type and status.
const { paymentLinks, total } = await arca.listPaymentLinks({ type: 'deposit', // optional filter status: 'pending', // optional filter});
for (const link of paymentLinks) { console.log(link.id, link.status, link.amount, link.denomination);}type'deposit' | 'withdrawal'statusstringpending, completed, expired.SDK: Operations & Events
getOperation(operationId, options?)
Get operation detail by ID, including correlated events and state deltas.
const detail = await arca.getOperation(operationId);console.log(detail.operation.type); // 'transfer', 'deposit', etc.console.log(detail.operation.state); // 'pending', 'completed', 'failed'console.log(detail.events); // correlated eventsconsole.log(detail.deltas); // state deltas (before/after values)includeEvidencebooleanconst detail = await arca.getOperation(operationId, { includeEvidence: true,});
console.log(detail.evidence?.journal.entries.length);console.log(detail.evidence?.journal.proofs);console.log(detail.evidence?.integrity.appendOnlyHistory);listOperations(opts?)
List operations in the realm, optionally filtered by type.
const { operations, total } = await arca.listOperations({ type: 'transfer', // single type filter types: ['fill', 'transfer'], // or multiple types (takes precedence) includeContext: true, // inline context (amount, fee, etc.)});typeOperationTypetransfer, create, delete, deposit, withdrawal, swap, order, fill, cancel.typesOperationType[]type.includeContextbooleanexportOperationEvidence(opts)
Export realm-scoped audit evidence over a time range, with optional operation-type and path filters.
let cursor: string | undefined;const pages = [];
do { const page = await arca.exportOperationEvidence({ from: '2026-04-01T00:00:00Z', to: '2026-04-05T23:59:59Z', types: ['transfer', 'deposit'], arcaPath: '/wallets/', limit: 100, cursor, });
pages.push(...page.operations); cursor = page.nextCursor ?? undefined;} while (cursor);fromstringrequiredtostringrequiredtypeOperationTypetypesOperationType[]type.arcaPathstringpathstringlimitnumber100; maximum 500.cursorstringnextCursor from the previous page.Evidence export in Phase 1 is journal-backed inspection data for builders and auditors. It improves traceability, but it is not yet a standalone cryptographic proof of faithful action.
nonce(prefix, separator?)
Reserve the next unique nonce for a path prefix. Returns a path suitable for use as an idempotency key. Always reserve before the operation and store the result. See Nonce Best Practices in the API Reference for separator conventions and anti-patterns.
const { path } = await arca.nonce('/op/transfer/fund');await arca.transfer({ path, from: '/wallets/alice', to: '/wallets/bob', amount: '100' });
// Colon separator for create-operation noncesconst { path: opPath } = await arca.nonce('/op/create/wallets/main', ':');listDeltas(arcaPath)
List state deltas for a given Arca path (before/after values for every state change).
const { deltas, total } = await arca.listDeltas('/wallets/main');summary()
Get aggregate counts for the realm.
const summary = await arca.summary();console.log(summary.objectCount); // 12console.log(summary.operationCount); // 47console.log(summary.eventCount); // 93Prefix 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(prefix, options?)
Get aggregated valuation for objects matching the prefix (or exact path).
// Aggregate all objects under a prefixconst agg = await arca.getPathAggregation('/users/alice/');console.log(agg.totalEquityUsd, agg.breakdown);
// Single objectconst single = await arca.getPathAggregation('/users/alice/exchanges/hl1');
// Historical aggregation at a past timestampconst hist = await arca.getPathAggregation('/users/', { asOf: '2026-02-15T00:00:00Z' });Response:
prefixstringtotalEquityUsdstringdepartingUsdstringarrivingUsdstringbreakdownAssetBreakdown[]asOfstring?Per-object valuations are not returned on this endpoint. For a one-shot read, use getObjectValuation(path). For streaming updates, use watchObject(path) or watchObjects(paths).
getPnl(prefix, from, to)
Get P&L for objects matching the prefix (or exact path) over a time range.
const pnl = await arca.getPnl('/users/alice/', '2026-02-01T00:00:00Z', '2026-03-01T00:00:00Z');console.log(pnl.pnlUsd); // e.g. "1200.00"Response:
prefixstringfromstringtostringstartingEquityUsdstringendingEquityUsdstringnetInflowsUsdstringnetOutflowsUsdstringpnlUsdstringendingEquity - startingEquity - netInflows + netOutflows.externalFlowsExternalFlowEntry[]getPnlHistory(prefix, from, to, points?)
Returns P&L time-series data for objects under a path prefix, adjusted for external flows (deposits/withdrawals). Each point includes both pnlUsd and equityUsd.
const history = await arca.getPnlHistory('/users/alice/', '2026-02-01T00:00:00Z', '2026-03-01T00:00:00Z', 100);console.log(history.pnlPoints[0].pnlUsd, history.pnlPoints[0].equityUsd);Response:
prefixstringfromstringtostringpointsnumberstartingEquityUsdstringeffectiveFromstring?effectiveFrom indicates where the series actually begins. The SDK uses this internally to align flowsSince for watchPnlChart.pnlPointsPnlPoint[]timestamp, pnlUsd, and equityUsd (strings, 2 decimal places).externalFlowsExternalFlowEntry[]getEquityHistory(prefix, from, to, points?)
Get equity time-series sampled evenly over a time range (default 200 points, max 1000).
const history = await arca.getEquityHistory('/users/', '2026-02-01T00:00:00Z', '2026-03-01T00:00:00Z', 100);Response:
prefixstringfromstringtostringpointsnumberequityPointsEquityPoint[]timestamp (string) and equityUsd (string, 2 decimal places).watchEquityChart(path, from, to, points?, options?)
Create a live equity chart that merges historical data with real-time aggregation updates. The rightmost point updates on each aggregation event (fills, balance changes, mid-price ticks). Returns an EquityChartStream.
const chart = await arca.watchEquityChart( '/users/alice/', '2026-02-01T00:00:00Z', '2026-03-01T00:00:00Z', 200,);
chart.onUpdate(({ points }) => { // points: sorted array of { timestamp, equityUsd } // Last point is always "now" with live equity renderEquityChart(points);});
chart.close();pathstringrequired/ for prefix).fromstringrequiredtostringrequiredpointsnumberoptions.exchangestring"sim").watchPnlChart(path, from, to, points?, options?)
Create a live P&L chart that merges historical P&L data with real-time aggregation updates and operation events. The rightmost point updates on each aggregation event. Deposits, withdrawals, and transfers that cross the path boundary are automatically tracked as external flows and subtracted from equity changes, so P&L reflects actual investment performance rather than cash movements.
const chart = await arca.watchPnlChart( '/users/alice/', '2026-02-01T00:00:00Z', '2026-03-01T00:00:00Z', 200,);
chart.onUpdate(({ points, externalFlows }) => { // points: sorted array of { timestamp, pnlUsd, equityUsd } // Last point is always "now" with live P&L renderPnlChart(points);
// externalFlows: itemized deposits/withdrawals/transfers // that crossed the path boundary console.log('Flows:', externalFlows);});
// startingEquityUsd is available for referenceconsole.log('Starting equity:', chart.startingEquityUsd);
chart.close();pathstringrequired/ for prefix).fromstringrequiredtostringrequiredpointsnumberoptions.exchangestring"sim").options.anchorstring"zero" (default) for standard P&L starting at 0, or "equity" to shift the chart so the live (rightmost) value equals the current account equity. When set to "equity", each point includes a valueUsd field suitable for the chart y-axis. The chart shape is identical to the P&L chart — only the y-axis is translated. Historical points remain stable during price movements; the offset only changes when a deposit or withdrawal completes.// Equity-anchored P&L chart (portfolio value view)const chart = await arca.watchPnlChart( '/users/alice/', '2026-02-01T00:00:00Z', '2026-03-01T00:00:00Z', 200, { anchor: 'equity' },);
chart.onUpdate(({ points }) => { // Use valueUsd for the y-axis — the live point equals current equity renderChart(points.map(p => ({ x: p.timestamp, y: p.valueUsd })));});How flow adjustments work: When a deposit or transfer operation completes, PnlChartStream detects the operation event via WebSocket, classifies it as an inflow or outflow relative to the watched path, and adjusts cumulative flows client-side. This means the P&L chart stays accurate across deposits and transfers without any additional server reads or manual recalculation.
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 (balances, positions, reserves), use watchObject(path) or watchObjects(paths).
createAggregationWatch(sources)
Create a watch that tracks objects matching the given sources. Returns the watch ID and an initial aggregation snapshot. After creation, the server pushes aggregation.updated events via WebSocket whenever an object in the watch set changes.
// Track all objects under a user prefixconst { watchId, aggregation } = await arca.createAggregationWatch([ { type: 'prefix', value: '/users/alice/' },]);console.log(aggregation.totalEquityUsd); // e.g. "15734.56"console.log(aggregation.breakdown); // per-asset rollupsourcesAggregationSourceDto[]requiredtype and a type-specific field (value for prefix/pattern/watch, paths for paths). Multiple sources are unioned together.Source Types
| Type | Value | Matches |
|---|---|---|
prefix | Path prefix with trailing slash | All objects whose path starts with the prefix (e.g., /users/alice/ matches /users/alice/main, /users/alice/exchanges/hl1) |
pattern | Glob with * wildcard | Single-segment wildcard matching (e.g., /users/*/exchanges/hl/* matches all users' Hyperliquid exchanges) |
paths | Array of paths (via paths field) | Exact path list (e.g., ['/treasury/usd', '/treasury/btc']) |
watch | Watch ID | Compose another watch — includes all objects tracked by the referenced watch |
// Combine multiple source types in a single watchconst { watchId } = await arca.createAggregationWatch([ { type: 'prefix', value: '/users/alice/' }, { type: 'paths', paths: ['/treasury/usd', '/treasury/btc'] }, { type: 'pattern', value: '/users/*/exchanges/hl/*' },]);watchAggregation(sources, options?)
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 .close() when done — it cleans up everything automatically.
const stream = await arca.watchAggregation([ { type: 'prefix', value: '/users/alice/' },]);
stream.onUpdate((agg) => { console.log('Equity:', agg.totalEquityUsd); console.log('Breakdown:', agg.breakdown);});
// Clean up when donestream.close();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.
Options: exchange (default 'sim') and flowsSince (ISO timestamp; when set, the server computes cumulative cumInflowsUsd/cumOutflowsUsd from that time instead of from watch creation — used internally by watchPnlChart).
Manual Wiring (Advanced)
For full control you can use createAggregationWatch() with manual WebSocket and price subscriptions. This is useful when you need custom throttling or want to share a single price stream across multiple consumers.
import { revalueAggregation } from '@arcaresearch/sdk';
// 1. Create the watch (get initial snapshot)const { watchId, aggregation } = await arca.createAggregationWatch([ { type: 'prefix', value: '/users/alice/' },]);let portfolio = aggregation;
// 2. Listen for structural updatesarca.ws.ensureConnected();arca.ws.on('aggregation.updated', (event) => { if (event.aggregation) { portfolio = event.aggregation; renderPortfolio(portfolio); }});
// 3. Revalue on mid price ticks for real-time USD valuesconst prices = await arca.watchPrices();prices.onUpdate((mids) => { portfolio = revalueAggregation(portfolio, mids); renderPortfolio(portfolio);});
// 4. Clean up when doneprices.close();arca.ws.off('aggregation.updated', handler);await arca.destroyAggregationWatch(watchId);getWatchAggregation(watchId)
Get the current aggregation for an existing watch (recomputed on demand). Useful for initial page loads or manual refreshes.
const agg = await arca.getWatchAggregation(watchId);console.log(agg.totalEquityUsd, agg.breakdown);destroyAggregationWatch(watchId)
Destroy a watch and stop receiving updates. Watches are also automatically evicted after 5 minutes of inactivity (no WebSocket messages referencing the watch).
await arca.destroyAggregationWatch(watchId);Response shape (PathAggregation):
prefixstring"(watch)" for watch-based aggregation.totalEquityUsdstringdepartingUsdstringarrivingUsdstringbreakdownAssetBreakdown[]waitForQuiescence(opts?)
Wait until all pending operations in the realm reach a terminal state. Auto-connects the WebSocket for faster resolution (reacts to settlement events instantly, with periodic HTTP polls as a safety net).
const polls = await arca.waitForQuiescence({ intervalMs: 1000, // default timeoutMs: 120_000, // default onPoll: (pending) => console.log(`${pending} operations pending`),});waitForOperation(operationId, timeoutMs?)
Wait for a specific operation to reach a terminal state. Automatically connects the WebSocket and watches the root path for events — no need to call ws.connect() manually. Uses WebSocket events for immediate resolution. Throws OperationFailedError if the operation ends in failed or expired state.
Note: In most cases, you don't need to call this directly — awaiting an OperationHandle calls it internally. Use it only when you have an operation ID from another source (e.g., a webhook or database).
import { OperationFailedError } from '@arcaresearch/sdk';
try { const completed = await arca.waitForOperation(operationId, 30_000); console.log(completed.state); // 'completed'} catch (err) { if (err instanceof OperationFailedError) { // Human-readable message safe to display to users console.error(err.message); // e.g. "Insufficient balance to complete this operation."
// Also available on the operation object console.error(err.operation.failureMessage);
// Raw outcome JSON for programmatic inspection console.error(err.operation.outcome); }}SDK: 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(opts)
Ensure a perps exchange Arca object exists at the given path. Denomination is automatically set to USD.
const { object } = await arca.ensurePerpsExchange({ ref: '/exchanges/hl1', exchangeType: 'hyperliquid', // default});refstringrequiredexchangeTypestringhyperliquid.operationPathstringgetExchangeState(objectId)
Get exchange account state including equity, margin, positions, and open orders. Use marginSummary for account equity and risk thresholds — see getActiveAssetData() below for computing max order sizes.
const state: ExchangeState = await arca.getExchangeState(objectId);console.log(state.marginSummary.equity); // equityconsole.log(state.marginSummary.initialMarginUsed); // initial margin in useconsole.log(state.marginSummary.maintenanceMarginRequired); // liquidation thresholdconsole.log(state.crossMaintenanceMarginUsed); // Hyperliquid-parity alias (cross mode)console.log(state.positions); // SimPosition[]console.log(state.openOrders); // SimOrder[]marginSummary Fields
The marginSummary object on ExchangeState contains derived account values. These are the authoritative balance fields for exchange accounts — there is no separate usdBalance field exposed.
equitystringtotalRawUsdstringavailableToWithdrawstringequity - initialMarginUsed for available trading margin, or watchMaxOrderSize() for per-coin max sizes.initialMarginUsedstringmaintenanceMarginRequiredstringtotalUnrealizedPnlstringtotalNtlPosstringupdateLeverage(opts)
Set the leverage for a coin. Leverage is a per-coin setting, not per-order. Changing leverage re-margins any existing position. Rejects if insufficient margin.
await arca.updateLeverage({ objectId: exchangeId, coin: 'hl:BTC', leverage: 10,});objectIdstringrequiredcoinstringrequiredhl:BTC).leveragenumberrequiredplaceOrder(opts)
Place a market or limit order. Returns an OrderHandle (extends OperationHandle) with fill lifecycle methods. The path is the idempotency key. When leverage is provided, the account's per-coin leverage is automatically set before placing the order (equivalent to calling updateLeverage then placeOrder). If omitted, the order uses whatever leverage is currently set for the coin on that account.
const order = arca.placeOrder({ path: '/op/order/btc-buy-1', objectId: exchangeId, coin: 'hl:BTC', side: 'BUY', orderType: 'MARKET', size: '0.01', leverage: 5,});await order; // wait for placement
// Wait for the order to be fully filledconst filled = await order.filled({ timeout: 30000 });console.log(filled.fills);
// Stream fills via async iteratorfor await (const fill of order.fills()) { console.log(`Filled ${fill.size} @ ${fill.price}`);}
// Callback-based fill listener (returns unsubscribe function)const unsub = order.onFill((fill) => console.log(fill));
// Cancel the orderconst cancelResult = await order.cancel();Max Size Orders
When a user selects “max” in your UI, pass useMax: true along with the displayed size as the reference. The server resolves the actual max at execution time, eliminating race conditions between client-side max computation and server-side margin checks.
// Get the current max from the live streamconst maxStream = await arca.watchMaxOrderSize({ objectId: exchangeId, coin: 'hl:BTC', side: 'BUY', leverage: 5,});const displayedMax = maxStream.value.activeAssetData.maxBuySize;
// Place with useMax — server resolves atomicallyconst order = arca.placeOrder({ path: '/op/order/btc-max-1', objectId: exchangeId, coin: 'hl:BTC', side: 'BUY', orderType: 'MARKET', size: displayedMax, // reference (what the user saw) useMax: true, // server resolves fresh max at execution time});The server will never fill more tokens than the reference size. If the resolved max is below the reference by more than 2% (default), the order is rejected with a clear message so the user can review the change. Builders can customize the tolerance:
arca.placeOrder({ // ... useMax: true, sizeTolerance: 0.05, // allow up to 5% downside deviation});sizeTolerance works on any order, not just useMax. When the order fails a margin check (e.g. price drift), the server reduces the size by up to the tolerance percentage. Only reduces, never increases. Recommended: 0.01 for interactive flows, 0.02 for retail. maxSizeTolerance is deprecated but still works as an alias.
Close Position (Market, close-only)
The preferred way to close a position is with closePosition(). It looks up the current position, infers the closing side, defaults to closing the full position, and always sets reduceOnly: trueso the order can never accidentally open or increase a position.
// Close a full BTC position (side + size inferred)await arca.closePosition({ path: '/op/close/btc-1', objectId: exchangeId, coin: 'hl:BTC',});
// Partial close — close 0.005 of a BTC positionawait arca.closePosition({ path: '/op/close/btc-partial', objectId: exchangeId, coin: 'hl:BTC', size: '0.005',});pathstringrequiredobjectIdstringrequiredcoinstringrequiredhl:BTC).sizestringtimeInForcestringIOC).If you need more control (limit price, custom side), use placeOrder directly with reduceOnly: true:
await arca.placeOrder({ path: '/op/order/btc-close-1', objectId: exchangeId, coin: 'hl:BTC', side: 'SELL', orderType: 'MARKET', size: '0.01', reduceOnly: true,});Reverse After Exit (two-step)
If your UX supports reversal, execute it as two operations: first a close-only exit, then a separate open order in the opposite direction. 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.
pathstringrequiredobjectIdstringrequiredcoinstringrequiredhl:BTC, hl:ETH).side'BUY' | 'SELL'requiredorderType'MARKET' | 'LIMIT'requiredsizestringrequiredpricestringLIMIT orders.leveragenumberreduceOnlybooleantimeInForcestringGTC (default), IOC, ALO.isTriggerbooleantriggerPx and tpsl are required.triggerPxstringisMarketbooleanprice as the limit price after trigger.tpsl'tp' | 'sl'tp) or stop loss (sl). Required when isTrigger is true.grouping'na' | 'normalTpsl' | 'positionTpsl'na (standalone), normalTpsl (fixed, parent-linked; OCO — one trigger cancels siblings), positionTpsl (tracks position size; cancels when position closes).Trigger orders surface as WAITING_FOR_TRIGGER and TRIGGERED in addition to standard order statuses.
listOrders(objectId, status?)
List orders for an exchange Arca object, optionally filtered by status.
const orders: SimOrder[] = await arca.listOrders(exchangeId);const openOrders = await arca.listOrders(exchangeId, 'OPEN');getOrder(objectId, orderId)
Get a specific order with its fill history.
const { order, fills }: SimOrderWithFills = await arca.getOrder(exchangeId, orderId);console.log(order.status); // 'FILLED'console.log(fills.length); // number of fillscancelOrder(opts)
Cancel an open order. Returns an OperationHandle. The path is the idempotency key. Prefer order.cancel() on an OrderHandle (auto-generates the cancel path).
await arca.cancelOrder({ path: '/op/cancel/btc-1', objectId: exchangeId, orderId: orderId,});pathstringrequiredobjectIdstringrequiredorderIdstringrequiredplaceTwap(opts)
Start a TWAP (Time-Weighted Average Price) order that executes a total size over a duration by placing market orders at regular intervals. Returns an OperationHandle.
const twap = await arca.placeTwap({ exchangeId: exchangeId, path: '/wallets/main/twap/btc-accumulate', coin: 'hl:BTC', side: 'BUY', totalSize: '10.0', durationMinutes: 120, intervalSeconds: 30,});console.log(twap.twap.status); // 'active'exchangeIdstringrequiredpathstringrequiredcoinstringrequired"hl:BTC").side'BUY' | 'SELL'requiredtotalSizestringrequireddurationMinutesnumberrequiredintervalSecondsnumberrandomizebooleanreduceOnlybooleanleveragenumberslippageBpsnumbercancelTwap(exchangeId, operationId)
Cancel an active TWAP. Returns an OperationHandle.
await arca.cancelTwap(exchangeId, operationId);getTwap(exchangeId, operationId)
Get TWAP status and progress by its parent operation ID.
const { twap } = await arca.getTwap(exchangeId, operationId);console.log(twap.executedSize, twap.filledSlices, twap.status);listTwaps(exchangeId, activeOnly?)
List TWAPs for an exchange object.
const twaps = await arca.listTwaps(exchangeId, true); // active onlygetTwapLimits()
Get TWAP limits and constraints for validation before placing a TWAP. Returns static platform limits.
const limits = arca.getTwapLimits();// { minSliceNotionalUsd: 10, minIntervalSeconds: 10, maxDurationMinutes: 43200, ... }listPositions(objectId)
List current positions for an exchange Arca object. Each position includes returnOnEquity (unrealized P&L / margin used) and positionValue (size × mark price), computed server-side.
const positions: SimPosition[] = await arca.listPositions(exchangeId);for (const pos of positions) { console.log(pos.coin, pos.side, pos.size, pos.unrealizedPnl); console.log('ROE:', pos.returnOnEquity, 'Value:', pos.positionValue);}listFills(objectId, opts?)
List historical fills (trades) for an exchange object with per-fill P&L, fee breakdown, trade direction, and resulting position state.
const { fills, cursor }: FillListResponse = await arca.listFills(exchangeId, { market: 'hl:BTC', limit: 50,});for (const fill of fills) { console.log(fill.market, fill.side, fill.size, fill.price); console.log(' dir:', fill.dir); // 'Open Long', 'Close Short', etc. console.log(' pnl:', fill.realizedPnl); console.log(' fees:', fill.exchangeFee, fill.platformFee, fill.builderFee); console.log(' position after:', fill.resultingPosition); console.log(' order op:', fill.orderOperationId); // originating order placement}marketstringhl:BTC).startTimestringendTimestringlimitnumbercursorstringwatchFills(objectId, opts?)
Subscribe to a live-updating trade history for an exchange object. Returns a FillWatchStream that combines an initial REST snapshot with real-time fill.recorded WebSocket events. Each fill is a platform-levelFill with P&L, fee breakdown, trade direction, and resulting position state.
const stream = await arca.watchFills(exchangeId, { market: 'hl:BTC' });
// Initial fills are available immediatelyconsole.log(stream.fills);
// Live updates as new fills arrivestream.onUpdate(({ fill, event }) => { console.log('New fill:', fill.market, fill.side, fill.size, fill.price); console.log(' dir:', fill.dir, 'pnl:', fill.realizedPnl);});
// Clean up when donestream.close();marketstringhl:BTC).limitnumbertradeSummary(objectId, opts?)
Get per-market P&L aggregation: total realized P&L, total fees, trade count, and volume, with cross-market totals.
const summary: TradeSummaryResponse = await arca.tradeSummary(exchangeId);for (const m of summary.markets) { console.log(m.market, 'pnl:', m.totalRealizedPnl, 'fees:', m.totalFees, 'trades:', m.tradeCount);}console.log('Overall:', summary.totals);startTimestringendTimestringFunding Events
Funding payments are applied hourly to open positions based on the current Hyperliquid funding rate. Use the convenience handler to listen for funding events:
// Callback-based funding listenerconst unsub = arca.ws.onExchangeFunding((funding, event) => { console.log(funding.coin, funding.side, funding.payment); // payment is signed: negative = account paid, positive = account received});
// Or listen for exchange.funding events directlyarca.ws.on('exchange.funding', (event) => { console.log(event.funding);});The FundingPayment type contains: accountId, coin, side (LONG/SHORT), size, price (mark price at time of funding), fundingRate, and payment (signed amount).
getLeverage(objectId, coin?)
Get leverage settings for a specific coin or all coins.
// Single coinconst setting = await arca.getLeverage(exchangeId, 'hl:BTC');
// All coinsconst settings = await arca.listLeverageSettings(exchangeId);getActiveAssetData(objectId, coin, builderFeeBps?, leverage?)
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.
const data = await arca.getActiveAssetData(exchangeId, 'hl:BTC');// Use maxBuySize / maxSellSize for order size slidersconst maxLong = parseFloat(data.maxBuySize);const maxShort = parseFloat(data.maxSellSize);
// Pass leverage to override the stored settingconst data10x = await arca.getActiveAssetData(exchangeId, 'hl:BTC', 0, 10);coinstringhl:BTC).leverage{'{'}type: LeverageType, value: number{'}'}type is "cross" or "isolated".maxBuySizestringmaxSellSizestringavailableToTradestringmaxBuyUsd/maxSellUsd.maxBuyUsdstringmaxSellUsdstringmarkPxstringfeeRatestring"0.00045" for 4.5 bps). Includes exchange taker fee (HIP-3 scaled), platform fee, and builder fee (when builderFeeBps is passed).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.
// "I want to spend $200 total from my balance at 10x"const b = Arca.orderBreakdown({ amount: '200', amountType: 'spend', // 'spend' | 'notional' | 'tokens' leverage: 10, feeRate: data.feeRate, // from ActiveAssetData (all-in) price: data.markPx, // mark for market, limit price for limit side: 'BUY', szDecimals: 5,});// b.tokens — position size in tokens (committed quantity)// b.notionalUsd — position exposure (~$1,991)// b.marginRequired — margin from balance (~$199)// b.estimatedFee — fee from balance (~$0.90)// b.totalSpend — total from balance (~$200)
// Leveraged buying power pattern:// UI shows "Buying power: $1000" (= availableToTrade * leverage)// User enters $500 → divide by leverage → amountType: 'spend'const leveragedBreakdown = Arca.orderBreakdown({ amount: String(500 / 10), // $50 balance spend amountType: 'spend', leverage: 10, feeRate: data.feeRate, price: data.markPx, side: 'BUY', szDecimals: 5,});Precision: tokens is the committed quantity submitted to the exchange. Dollar values are estimates at the provided price. For limit orders (where price is the limit), the breakdown is exact contingent on fill. For market orders, actual fill price may differ.
getAssetFees(objectId, builderFeeBps?)
Get the effective taker and maker fee rates for every tradeable asset on an exchange account. Rates include the account's volume tier, per-asset HIP-3 fee scale, platform fee, and optional builder fee.
const fees = await arca.getAssetFees(exchangeId);for (const entry of fees) { console.log(entry.coin, 'taker:', entry.takerFeeRate, 'maker:', entry.makerFeeRate);}
// With builder fee (40 = 4.0 bps in tenths-of-bps units)const feesWithBuilder = await arca.getAssetFees(exchangeId, 40);coinstring"hl:BTC").takerFeeRatestring"0.00045" = 4.5 bps).makerFeeRatestring"0.00015" = 1.5 bps).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 (1d) candles open at UTC midnight boundaries. Use Date.now() for the current time — no timezone conversion is needed.
Other API timestamps (operations, events, objects) use RFC 3339 UTC strings (e.g. 2026-03-31T14:30:00.000000Z). Only market data uses numeric epoch milliseconds.
Market Metadata
Global market data endpoints — not scoped to a specific exchange object. Call getMarketMeta() to discover supported assets and their constraints. Use this data for client-side order validation before submission.
getMarketMeta()
Returns the full perps universe with per-asset constraints.
const meta = await arca.getMarketMeta();for (const asset of meta.universe) { console.log(asset.name, asset.maxLeverage, asset.szDecimals);}Each SimMetaAsset in universe contains:
namestring"hl:BTC", "hl:ETH", or "xyz:TSLA" for HIP-3 DEX assets).symbolstring"BTC", "ETH").exchangestring"hl" for Hyperliquid).dexstring?szDecimalsnumberszDecimals: 4 means sizes must be multiples of 0.0001. Truncate or round order sizes to this precision before submitting.maxLeveragenumberonlyIsolatedbooleantrue, only isolated margin is supported — cross-margin is not available for this asset.feeScalenumber?1 for standard perps; greater than 1 for builder-deployed perps where the deployer set a deployerFeeScale. Omitted or undefined for standard perps. Use this to display total effective fees to end users.candleHistoryobject?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. Omitted when bounds are not yet computed.displayNamestring?"Bitcoin", "Crude Oil", "S&P 500"). Use for UI labels; fall back to symbol when absent. Curated via the admin panel.logoUrlstring?isHip3boolean?deployerDisplayNamestring?Other Market Endpoints
// Current mid prices (keys are canonical market IDs)const mids: SimMidsResponse = await arca.getMarketMids();// { mids: { 'hl:BTC': '98234.50', 'hl:ETH': '2847.30', ... } }
// 24h tickers (volume, price change, funding, fee scale, delisted status)const tickers = await arca.getMarketTickers();// tickers.tickers[0].funding => current funding rate// tickers.tickers[0].nextFundingTime => Unix ms timestamp of next funding event (build a countdown from this)// tickers.tickers[0].feeScale => HIP-3 fee multiplier (1.0 for standard perps, >1 for builder-deployed)
// L2 order bookconst book: SimBookResponse = await arca.getOrderBook('hl:BTC');// { coin: 'hl:BTC', bids: [...], asks: [...], time: 1234567890 }
// OHLCV candles (supports 1m, 5m, 15m, 1h, 4h, 1d)const candles = await arca.getCandles('hl:BTC', '1m', { startTime: Date.now() - 3600000 });// candles.candles[0] => { t, o, h, l, c, v, n, s? }// The optional 's' field indicates data source:// undefined/absent = venue-native (Hyperliquid perps)// "ext" = external historical data (pre-listing reference prices)// Older candles (before Hyperliquid listing) may use external reference// data when fetched via CDN for 1d/4h intervals.
// 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.const sparklines = await arca.getSparklines();// sparklines.sparklines['hl:BTC'] => [60100.5, 60200.1, ...] (24 hourly close prices)// Newly listed coins may have fewer points until candle history accumulates.watchPrices(exchange?)
Subscribe to real-time mid prices for all assets. The server sends current prices as an initial snapshot, then streams updates. Returns a PriceWatchStream whose .prices property always reflects the latest values.
const stream = await arca.watchPrices();// stream.prices => { 'hl:BTC': '95432.50', 'hl:ETH': '3210.10', ... }
const unsub = stream.onUpdate((prices) => { renderAssetList(prices);});
// When done, close the stream (releases the subscription)stream.close();exchangestring"sim".Trading Recipe: End-to-End
This recipe walks through a complete trading integration in 7 steps: create accounts, fund them, place a leveraged trade, stream live updates, close the position, and withdraw profits. Copy this script and adapt it to your use case.
import { Arca } from '@arcaresearch/sdk';
const arca = new Arca({ apiKey: 'ak_...', realm: 'my-realm' });await arca.ready();
// 1. Create a wallet to hold fundsconst { nonce: n1 } = await arca.nonce('/demo/wallet');const wallet = await arca.ensureDenominatedArca({ path: '/demo/wallet', nonce: n1 });
// 2. Create an exchange accountconst { nonce: n2 } = await arca.nonce('/demo/exchange');const exchange = await arca.ensurePerpsExchange({ ref: '/demo/exchange', nonce: n2 });
// 3. Fund the wallet and transfer to exchangeawait arca.fundAccount({ objectId: wallet.arcaId, amount: '10000' });const { nonce: n3 } = await arca.nonce('/demo/fund-exchange');await arca.transfer({ from: wallet.arcaId, to: exchange.arcaId, amount: '5000', nonce: n3 });
// 4. Set leverage and place an orderawait arca.updateLeverage({ objectId: exchange.arcaId, coin: 'hl:BTC', leverage: 5 });const order = await arca.placeOrder({ objectId: exchange.arcaId, coin: 'hl:BTC', side: 'buy', size: '0.1', orderType: 'market',});const fill = await order.filled();console.log('Filled at', fill.price);
// 5. Stream exchange state for live updatesconst stream = await arca.watchExchangeState(exchange.arcaId);stream.onUpdate((state) => console.log('Equity:', state.equity));
// 6. Close the positionawait arca.closePosition({ objectId: exchange.arcaId, coin: 'hl:BTC' });
// 7. Transfer remaining balance back and clean upconst balances = await arca.getBalancesByObject(exchange.arcaId);const { nonce: n4 } = await arca.nonce('/demo/withdraw-profits');await arca.transfer({ from: exchange.arcaId, to: wallet.arcaId, amount: balances[0].settled, nonce: n4 });stream.close();Notes:
- Coin IDs must be in canonical format (e.g.
"hl:BTC", not"BTC"). fundAccount()is a dev helper for demo/development realms. In production, use payment links or on-chain deposits.- Leverage defaults to 1x if not set. Call
updateLeverage()before placing leveraged orders. - Each
nonce()call returns a unique identifier for idempotency. Never reuse a nonce or generate nonces in a retry loop — see Troubleshooting.
SDK: Real-time Streaming
The SDK provides declarative watch*() methods that handle WebSocket connection, authentication, subscriptions, and cleanup automatically. Each method returns a stream object with an initial snapshot and live updates. The underlying WebSocket is ref-counted — multiple watchers share a single connection, and the connection auto-disconnects after 60 seconds of idle time.
Scoped Token Compatibility
All watch*() methods work with both API keys and scoped JWT tokens. When using Arca.fromToken(scopedJwt), the WebSocket authenticates with the same token and respects its scope. Streaming methods receive events only for resources the token has read access to — the server-side scope enforcement applies to both REST and WebSocket delivery identically.
watchPrices(exchange?)
Subscribe to real-time mid prices. Returns a PriceWatchStream.
const stream = await arca.watchPrices();console.log(stream.prices); // { 'hl:BTC': '95432.50', 'hl:ETH': '3210.10' }
stream.onUpdate((prices) => renderAssetList(prices));
// Async iterationfor await (const prices of stream) { console.log(prices);}
stream.close(); // release the subscriptionwatchOperations()
Subscribe to real-time operation creates and updates. Returns an OperationWatchStream.
const stream = await arca.watchOperations();console.log(stream.operations); // initial list of recent operations
stream.onUpdate(({ operation, event }) => { if (operation.state === 'failed' || operation.state === 'expired') { console.error('Operation failed:', operation.failureMessage); } else if (operation.state === 'completed') { console.log('Settled:', operation.type, operation.path); }});
stream.close();watchBalances(arcaRef?)
Use watchBalances() for any UI or service that displays account balances. It delivers an initial snapshot plus real-time updates as deposits, withdrawals, and transfers settle — no polling or manual refresh needed. Use getBalancesByPath() only when you need a one-shot read (e.g., a CLI script or batch job).
Watch real-time balance updates, optionally filtered by Arca path prefix. Returns a BalanceWatchStream whose .balances Map always reflects the latest state for every object in scope. The server sends a full snapshot on watch, then pushes incremental balance.updated events as balances change.
// Watch all balances in the realmconst stream = await arca.watchBalances();
// Or filter to a specific path prefixconst stream = await arca.watchBalances('/wallets/main');
// Initial snapshot is available immediately after awaitconsole.log(stream.balances);// Map(2) {// 'obj_01h2...' => { entityId: 'obj_01h2...', entityPath: '/wallets/main', balances: [...] },// 'obj_01h3...' => { entityId: 'obj_01h3...', entityPath: '/wallets/savings', balances: [...] },// }
// Live updates as balances changestream.onUpdate(({ entityId, entityPath, balances, event }) => { console.log(`Balance changed on ${entityPath}`, balances); // balances: [{ id: '...', arcaId: '...', denomination: 'USD', amount: '1250.00' }]});
// Async iteration also worksfor await (const update of stream) { updateUI(update.entityPath, update.balances); if (done) break;}
stream.close();arcaRefstringTypes:
interface BalanceSnapshot { entityId: string; // Arca object ID entityPath?: string; // Arca object path (e.g. '/wallets/main') balances: ArcaBalance[];}
interface BalanceUpdate { entityId: string; entityPath?: string; balances?: ArcaBalance[]; // current balances after the change event: RealmEvent; // the raw WebSocket event}getObjectValuation(path)
Get the valuation for a single Arca object. Uses the same computation path as aggregation, guaranteeing perfect additivity (Axiom 10: Observational Consistency).
const valuation = await arca.getObjectValuation('/strategies/alpha');console.log('Equity:', valuation.valueUsd);watchObject(path)
Watch real-time valuation updates for a single Arca object. Returns an ObjectWatchStream that emits ObjectValuation on every structural change and on every mid price tick. The SDK automatically watches mid prices and revalues positions client-side, so valueUsd, unrealizedPnl, and markPrice update in real time.
const stream = await arca.watchObject('/strategies/alpha');
stream.onUpdate((valuation) => { console.log('Equity:', valuation.valueUsd); console.log('Positions:', valuation.positions);});
stream.close();watchObjects(paths)
Watch real-time valuations for multiple Arca objects at once. Returns an ObjectsWatchStream that emits a Map<path, ObjectValuation> on every update from any watched object (structural changes and mid-price ticks). Call .close() to stop all underlying watches.
const stream = await arca.watchObjects([ '/users/alice/main', '/users/bob/main',]);
stream.onUpdate((byPath) => { for (const [path, valuation] of byPath) { console.log(path, valuation.valueUsd); }});
stream.close();revalueObject(obj, mids) / revalueAggregation(agg, mids)
Standalone utility functions for client-side recomputation using fresh mid prices. revalueObject updates price-derived fields on an ObjectValuation (valueUsd, unrealizedPnl, markPrice). revalueAggregation recomputes PathAggregation from breakdown: spot entries are revalued with amount × mid; exchange and perp entries pass through server values until the next server push. departingUsd and arrivingUsd are USD pass-through and unchanged. Used internally by watchObject, watchObjects, and watchAggregation, and exported for manual wiring.
import { revalueObject, revalueAggregation } from '@arcaresearch/sdk';
const revalued = revalueObject(valuation, { BTC: '60000', ETH: '3000' });const revaluedAgg = revalueAggregation(aggregation, mids);watchMaxOrderSize(opts)
Subscribe to a live, SDK-derived max order size stream for a specific coin and side. The SDK recomputes on mid price changes and exchange state changes (orders, fills, leverage, deposits, funding payments) for responsive UI sizing, while order submission remains backend-authoritative. For HIP-3 assets, pass feeScale explicitly or omit it — the SDK will auto-fetch the correct scale from the tickers endpoint.
const stream = await arca.watchMaxOrderSize({ objectId: exchangeId, coin: 'hl:BTC', side: 'BUY', leverage: 5, builderFeeBps: 40, // 4.0 bps (tenths of a bps) szDecimals: 5, // feeScale: omit to auto-fetch from tickers, or pass explicitly for HIP-3 assets});
stream.onUpdate(({ activeAssetData }) => { console.log('max buy size', activeAssetData.maxBuySize); console.log('mark', activeAssetData.markPx);});
stream.close();watchCandleChart(coin, interval, options?)
Create a managed candle chart that merges historical data with live WebSocket updates. Returns a CandleChartStream with a sorted, deduped candle array and range-loading methods. Overlapping range requests are coalesced so rapid pan/zoom gestures do not drop history loads.
The count option sets the time window to fetch (now - interval * count). The actual number of candles returned depends on available history — recently listed assets or short-lived instruments may have fewer candles than requested. Use candleHistory from getMarketMeta() to check the earliest available candle timestamp for an asset.
const chart = await arca.watchCandleChart('hl:BTC', '1m', { count: 300 });
chart.onUpdate((update) => { renderChart(update.candles); // full sorted array});
// Load a specific time range (zoom, resize, jump to date):const result = await chart.ensureRange(startTime, endTime);// result.loadedCount — new candles fetched (0 if already loaded, or if an// overlapping in-flight ensureRange already finished covering this range)// result.reachedStart — true if no more history exists
// Load older candles (backward scroll):const more = await chart.loadMore(200);
chart.close();watchCandles(coins, intervals)
Low-level candle event stream. Each event contains a single candle — your app must maintain the chart array manually. For candlestick charts, use watchCandleChart() above instead. Returns a CandleWatchStream.
const stream = await arca.watchCandles(['hl:BTC', 'hl:ETH'], ['1m', '5m']);
stream.onUpdate((event) => { console.log(event.coin, event.interval, event.candle);});
stream.close();watchExchangeState(objectId)
Subscribe to real-time exchange state updates for an exchange Arca object. Returns an ExchangeWatchStream that includes positions, open orders, margin summary, and pending intents — order operations that have been submitted but haven't been confirmed by the venue yet.
Position P&L, equity, and margin summary are automatically revalued client-side on every mid-price tick for near-real-time updates. Server-pushed events (fills, orders, funding) provide the structural base. The source field on each update ('exchange' or 'mids') indicates what triggered it.
Pending intents are the exchange equivalent of transfer holds. They let your UI show that something is in progress without any additional code.
const stream = await arca.watchExchangeState(exchangeId);
// Initial state is available immediatelyconsole.log(stream.exchangeState?.positions);console.log(stream.pendingIntents); // orders in flight
stream.onUpdate(({ state, source }) => { renderPositions(state.positions); renderOpenOrders(state.openOrders);
// P&L updates arrive on every mid-price tick (source='mids') // and on structural changes like fills (source='exchange') console.log(source, state.marginSummary.equity);
// Show pending operations (submitted but not yet confirmed) for (const intent of state.pendingIntents) { showPending(intent.coin, intent.side, intent.size); }});
stream.close();Optimistic State
The SDK provides two mechanisms to show immediate feedback when operations are submitted, before the server confirms.
Balance Predictions (Transfers)
BalanceWatchStream.applyPrediction() immediately shows predicted departing and arriving amounts on the balance snapshot. The prediction is reconciled when the server's real hold arrives, or rolled back if the operation fails.
const balanceStream = await arca.watchBalances();const handle = arca.transfer({ path: '/op/t/1', from: '/wallets/main', to: '/wallets/savings', amount: '100' });
// Immediately shows departing amount on source, arriving on destinationbalanceStream.applyPrediction(handle);Operation Tracking
OperationWatchStream.trackSubmission() adds a synthetic operation entry the moment you call a mutation method, before the HTTP call completes. The operation transitions throughpending → completed/failed as events arrive.
const opStream = await arca.watchOperations();const handle = arca.transfer({ path: '/op/t/2', from: '/wallets/main', to: '/wallets/savings', amount: '50' });
// Immediately shows a pending entry in opStream.operationsopStream.trackSubmission(handle);Auto-Tracking
For the simplest setup, use enableAutoTracking() to automatically apply predictions and track submissions on every mutation:
const opStream = await arca.watchOperations();const balanceStream = await arca.watchBalances();const exchangeStream = await arca.watchExchangeState(exchangeId);
// One line — all future mutations get automatic optimistic feedbackarca.enableAutoTracking(opStream, balanceStream, exchangeStream);
// Now every transfer, placeOrder, closePosition automatically shows// immediate feedback in the registered streamsconst handle = arca.transfer({ ... });// balanceStream immediately shows departing amount// opStream immediately shows pending operationStream API
All watch streams share these capabilities:
| Property / Method | Description |
|---|---|
.onUpdate(cb) | Register a callback for each update. Returns an unsubscribe function. |
.close() | Stop listening and release the underlying subscription. |
.isClosed | Whether the stream has been closed. |
for await (const x of stream) | Async iteration. Breaking out of the loop calls .close(). |
await using stream = ... | Explicit resource management via Symbol.asyncDispose. |
React Hooks
The @arcaresearch/react package provides hooks that wrap each watch*() method with React lifecycle management. Cleanup is automatic on unmount.
npm install @arcaresearch/reactimport { ArcaProvider, useArcaPrices, useArcaOperations } from '@arcaresearch/react';
function App() { return ( <ArcaProvider arca={arca}> <AssetList /> <OperationFeed /> </ArcaProvider> );}
function AssetList() { const { prices, status } = useArcaPrices(); if (status === 'loading') return <div>Loading...</div>; return <div>{JSON.stringify(prices)}</div>;}
function OperationFeed() { const { operations, status } = useArcaOperations(); return <ul>{operations.map(op => <li key={op.id}>{op.state}</li>)}</ul>;}Available hooks: useArcaPrices, useArcaOperations, useArcaBalances, useArcaCandles.
Preventing Subscription Leaks
Every watch*() call acquires a subscription. If the stream is not closed, the subscription remains active and the WebSocket stays open. Follow these patterns to avoid leaks:
// 1. Always close in a finally blockconst stream = await arca.watchOperations();try { for await (const update of stream) { // process... if (done) break; // break calls .close() automatically }} finally { stream.close(); // idempotent — safe to call even if already closed}
// 2. Use explicit resource management (TypeScript 5.2+){ await using stream = await arca.watchPrices(); stream.onUpdate((prices) => { /* ... */ }); // stream.close() is called automatically when the block exits}
// 3. React — use hooks from @arcaresearch/react// Cleanup is automatic on unmount. No manual close() needed.
// 4. Swift — use structured concurrency// for await in a Task scope auto-cleans up on task cancellation.Common pitfall: calling watch*() inside a loop or repeated function without closing previous streams. Each call creates a new subscription — always close the previous stream before creating a new one.
What the SDK does for you: even without perfect cleanup, the system degrades gracefully. Unwatches are debounced (100ms) to prevent chatter during rapid mount/unmount cycles. Shared watches are ref-counted — the underlying watch only closes when all watchers release. The WebSocket auto-disconnects after 60 seconds with zero active watchers.
Low-Level WebSocket API
The arca.ws WebSocketManager is still available for advanced use cases. The typed on*() methods and manual watchPath()/unwatchPath() calls provide path-scoped event delivery with snapshot support. For most use cases, prefer the watch*() methods above.
SDK: Error Handling
The SDK maps API error responses to typed error classes. All errors extend ArcaError, which provides a machine-readable code and a human-readable message.
Error Classes
| Class | Code | HTTP Status | When |
|---|---|---|---|
ValidationError | VALIDATION_ERROR | 400 | Invalid input |
UnauthorizedError | UNAUTHENTICATED | 401 | Missing or invalid auth |
NotFoundError | NOT_FOUND | 404 | Resource not found |
ConflictError | CONFLICT | 409 | Duplicate or invalid state |
InternalError | INTERNAL_ERROR | 500 | Unexpected server error |
Usage Pattern
import { Arca, ValidationError, NotFoundError, ConflictError } from '@arcaresearch/sdk';
try { await arca.transfer({ path: '/op/transfer/fund-1', from: '/wallets/alice', to: '/wallets/bob', amount: '250.00', });} catch (err) { if (err instanceof ValidationError) { console.error('Invalid input:', err.message); // e.g., insufficient balance, denomination mismatch } else if (err instanceof NotFoundError) { console.error('Object not found:', err.message); } else if (err instanceof ConflictError) { console.error('Conflict:', err.message); // e.g., duplicate resource, object being deleted } else { throw err; // unexpected }}Base Error Class
class ArcaError extends Error { public readonly code: string; // machine-readable code public readonly message: string; // human-readable description}All error classes (ValidationError, UnauthorizedError, etc.) extend ArcaError and set the appropriate code. Unknown error codes are returned as a base ArcaError instance.
Advanced: Authentication & Org Management ▸
SDK: Authentication
The ArcaAdmin class provides authentication methods including sign-in, sign-up, token refresh, and logout.
Refresh Token
const admin = new ArcaAdmin({ baseUrl: 'https://api.arcaos.io' });const { token, refreshToken, expiresAt } = await admin.refresh(oldRefreshToken);// Use the new token for subsequent requestsadmin.setToken(token);Logout
const admin = new ArcaAdmin({ baseUrl: 'https://api.arcaos.io', token });await admin.logout(refreshToken);// Refresh token is revoked server-side; access token expires in ≤15 minSign In with Google
Sign in with Google OAuth. Submit the Google ID token from your OAuth flow. Returns the same AuthResponse as sign-in. For new users, provide orgName to create an organization on first sign-in; otherwise the API returns error code new_account_requires_org.
const { token, refreshToken, builder } = await admin.signInWithGoogle( googleIdToken, 'My Org', // optional — required for new accounts);googleIdTokenstringrequiredorgNamestringSDK: Org & Team Management
The ArcaAdmin class provides methods for managing your organization, team members, and invitations.
Create Organization
Create a new organization. The authenticated user becomes the owner. Returns the organization and membership.
const { organization, membership } = await admin.createOrg('Acme Inc');console.log(organization.id, organization.slug);console.log(membership.role); // 'owner'namestringrequiredGet Organization
const admin = new ArcaAdmin({ token });const org = await admin.getOrg();console.log(org.name, org.slug);List Members
const { members, total } = await admin.listMembers();for (const m of members) { console.log(m.email, m.role, m.realmTypeScope);}Invite a Member
const invitation = await admin.inviteMember({ email: 'alice@example.com', role: 'developer', realmTypeScope: ['development', 'staging'],});console.log(invitation.inviteLink);Update Member Role
await admin.updateMember('member-id', { role: 'admin', realmTypeScope: null, // null = all realm types});Remove Member
await admin.removeMember('member-id');Transfer Ownership
await admin.transferOwnership('new-owner-user-id');Custody
Arca uses an on-chain custody contract (ArcaCustodyPool on HyperEVM) for non-custodial fund isolation. The SDK provides read access to custody state and transaction preparation helpers for user-signed sovereignty operations.
Get Custody Status
Returns the full custody state for the realm: contract address, total balance, all isolation boundaries, exchange arcas, and any active venue halts.
const status = await arca.getCustodyStatus();console.log(status.contractAddress, status.totalBalance);console.log(status.boundaries);console.log(status.exchangeArcas);Get Boundary
const boundary = await arca.getBoundary('b0');console.log(boundary.balance, boundary.status, boundary.recoveryKey);boundaryIdstringrequiredList Boundaries
const boundaries = await arca.listBoundaries();for (const b of boundaries) { console.log(b.id, b.balance, b.status);}List Exchange Arcas
// All exchange arcasconst arcas = await arca.listExchangeArcas();
// Filtered to a specific boundaryconst filtered = await arca.listExchangeArcas('b0');boundaryIdstringRecovery Keys
A recovery key gives a user an independent exit path: the ability to force-withdraw funds from the custody contract without depending on Arca. There are two ways to set one up.
Path A: Use an Existing Wallet (Recommended)
If the user already has an Ethereum wallet (MetaMask, Phantom, Ledger, WalletConnect, etc.), they can register that wallet's address directly. No new key management is required — the user is already responsible for that wallet.
// User pastes or connects an existing wallet addressawait arca.registerRecoveryKey({ boundaryId: 'b0', walletAddress: '0x1234...abcd', // from WalletConnect, paste, etc.});Path B: Generate a New Recovery Wallet
For users who don't have an existing wallet, the SDK can generate a BIP-39 mnemonic and derive an Ethereum address client-side. The mnemonic never leaves the user's device and is never sent to any server.
const recovery = Arca.generateRecoveryKey();// recovery.mnemonic — 12-word BIP-39 phrase (back this up!)// recovery.address — derived Ethereum address (EIP-55 checksummed)
// CRITICAL: the user must back up the mnemonic before proceeding.// Show it to the user with a confirmation step.
await arca.registerRecoveryKey({ boundaryId: 'b0', walletAddress: recovery.address,});Arca.generateRecoveryKey() is a static method — it requires no network call and no initialized Arca instance. It uses the platform CSPRNG (crypto.getRandomValues) for entropy and derives the key at the standard BIP-44 path (m/44'/60'/0'/0/0). All intermediate cryptographic material (seed, private key bytes) is zeroed after derivation.
Backup Guidance by Platform
| Platform | Path A (existing wallet) | Path B (generated mnemonic) |
|---|---|---|
| iOS | WalletConnect or paste address | Store in Keychain with kSecAttrSynchronizable (syncs via iCloud) |
| Android | WalletConnect or paste address | Store in EncryptedSharedPreferences with Google backup |
| Web | WalletConnect, injected provider, or paste address | “Write down these 12 words” ceremony with verification |
Do not store mnemonics in localStorage, cookies, or unencrypted browser storage. These are vulnerable to XSS, browser extensions, and are not encrypted at rest.
registerRecoveryKey(opts)
Assigns a recovery key to an isolation boundary. Once set, only the recovery key holder can lock, unlock, or withdraw from the boundary. This is an operator-initiated action that works only when no recovery key is currently set.
boundaryIdstringrequiredwalletAddressstringrequiredArca.generateRecoveryKey().address).Prepare Sovereignty Transactions
These methods return unsigned EVM transactions (PreparedCustodyTransaction) that the user signs and submits from their recovery key wallet. Compatible with ethers.js, viem, MetaMask, or any Ethereum wallet.
const { contractAddress, chainId } = await arca.getCustodyStatus();const boundaryId = '0x' + 'b0'.padStart(64, '0');
// Lock boundary (freezes operator fund movement, cancels pending exits)const lockTx = arca.prepareLockBoundary(contractAddress, chainId, boundaryId);
// Unlock boundary (only callable by the actor who locked it)const unlockTx = arca.prepareUnlockBoundary(contractAddress, chainId, boundaryId);
// Withdraw entire boundary balance (requires locked by caller)const withdrawTx = arca.prepareWithdraw(contractAddress, chainId, boundaryId);
// Change recovery key (only current holder)const keyTx = arca.prepareAssignRecoveryKey( contractAddress, chainId, boundaryId, newAddress,);
// Set time lock on boundary exits (recovery key can only increase)const timeLockTx = arca.prepareSetTimeLock( contractAddress, chainId, boundaryId, 86400,);
// Cancel a pending cross-boundary transferconst cancelTx = arca.prepareCancelPendingExit( contractAddress, chainId, boundaryId,);
// Sweep exchange arca balance to boundary (requires locked)const sweepTx = arca.prepareRecoveryWithdrawAccount( contractAddress, chainId, arcaId,);Each returns {to, data, chainId, value}. Submit via your preferred Ethereum library or wallet.