Arca/Documentation
Join Waitlist

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

bash
npm install @arcaresearch/sdk

Package Exports

The package exports one main class and all supporting types:

typescript
import { Arca } from '@arcaresearch/sdk';
// Types are also exported for strong typing
import 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).

typescript
// API Key — for backend services, scripts, CI
const arca = new Arca({
apiKey: 'arca_78ae7276_...',
realm: 'development', // slug or UUID
baseUrl: 'http://localhost:3052', // optional
});
await arca.ready();
// Scoped Token — for end-user frontends
const arca = Arca.fromToken(scopedJwt);
// realm is extracted from the token claims automatically
await 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.

typescript
import { Arca } from '@arcaresearch/sdk';
const arca = new Arca({
apiKey: 'arca_78ae7276_...',
realm: 'development',
});
apiKeystringrequired
Full API key including prefix and secret.
realmstringrequired
Realm slug (e.g., development) or UUID.
baseUrlstring
API base URL. Default: https://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.

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

typescript
const arca = Arca.fromToken(scopedJwt, { realm: 'development' });
tokenstring
Scoped JWT issued by POST /auth/token. Optional when tokenProvider is set.
tokenProvider() => Promise<string>
Async function returning a fresh scoped JWT. When set, the SDK refreshes automatically before expiry, retries on 401, and fetches fresh tokens on WebSocket reconnect.
realmstring
Realm slug or UUID. Omit if the token contains a realmId claim.
baseUrlstring
API base URL. Default: https://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.

ContextAuth modeScope
Backend service, CI, scriptsnew 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
typescript
// Recommended: pass a token provider for automatic refresh
const 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.

typescript
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).

typescript
const unsub = arca.onAuthError((error) => {
console.error('Session expired:', error.message);
redirectToLogin();
});
// Call unsub() to remove the listener

History Cache

The SDK caches responses from getEquityHistory(), getPnlHistory(), and getCandles() in an in-memory LRU cache. This eliminates redundant network calls when switching between charts or toggling intervals. The watchEquityChart() 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:

typescript
// Custom cache size
const arca = new Arca({
apiKey: 'arca_78ae7276_...',
realm: 'development',
cache: { maxEntries: 100 },
});
// Disable caching entirely
const 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:

typescript
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-user
const { token: scopedJwt } = await admin.mintToken({
realmId: '6d25623e-...',
sub: 'alice',
scope: {
statements: [
{ effect: 'Allow', actions: ['arca:Read'], resources: ['*'] },
],
},
expirationMinutes: 30,
});
// Return scopedJwt to the frontend

SDK: 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

typescript
// Resolves when the fund operation has fully settled
await 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:

typescript
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 timeout
await handle.wait({ timeout: 15000 });

Batching

Handles work with Promise.all for parallel operations:

typescript
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:

typescript
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 fills
const filled = await order.filled({ timeout: 30000 });
console.log(filled.fills);
// Stream fills via async iterator
for 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 listener
const 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 / MethodAvailable OnDescription
await handleOperationHandleWait for full settlement
handle.submittedOperationHandlePromise for the HTTP response (before settlement)
handle.wait({ timeout })OperationHandleWait for settlement with explicit timeout
order.filled({ timeout })OrderHandleWait until order is fully filled
order.fillSummary({ timeout })OrderHandleGet fill with P&L, fee breakdown, direction, and resulting position
order.fills()OrderHandleAsync iterator yielding fills as they arrive
order.onFill(cb)OrderHandleCallback for each fill; returns unsubscribe
order.cancel()OrderHandleCancel the order

SDK: Arca Objects

Path Hierarchy & Aggregation/payments/payments/users/payments/fees/payments/users/alice/payments/users/bob↑ aggregation rolls upLeaf nodes hold balances. Query any prefix to get aggregated totals.GET /aggregation?path=/payments → total across all users + fees

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 OperationHandleawait resolves when the object is active (immediate for existing objects, waits for creation workflow otherwise).

typescript
const { object } = await arca.ensureDenominatedArca({
ref: '/wallets/main',
denomination: 'USD',
operationPath: '/op/create/wallets/main:1', // optional idempotency key
});
refstringrequired
Full Arca path.
denominationstringrequired
Currency or asset denomination (e.g., USD, BTC).
metadatastring
Optional JSON metadata string.
operationPathstring
Operation path for explicit idempotency (use the nonce API to generate).

ensureArca(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().

typescript
const { object } = await arca.ensureArca({
ref: '/deposits/d1',
type: 'deposit',
denomination: 'USD',
});
refstringrequired
Full Arca path.
typestringrequired
One of: denominated, exchange, deposit, withdrawal, escrow.
denominationstring
Required for denominated type.
metadatastring
Optional JSON metadata string.
operationPathstring
Optional operation-level idempotency key.

getObject(path)

Get the active Arca object at a given path.

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

typescript
const detail = await arca.getObjectDetail(obj.id);
console.log(detail.operations.length); // all operations on this object
console.log(detail.balances); // current balances
console.log(detail.reservedBalances); // in-flight reserved amounts
console.log(detail.object.isolation); // boundary metadata, when applicable

listObjects(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.

typescript
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:

settledstring
Funds available for new operations.
arrivingstring
Funds in transit toward this object (e.g., exchange deposit awaiting venue confirmation). Always "0.00" for denominated-to-denominated transfers.
departingstring
Funds committed to leave via an outbound transfer that has not yet settled.
totalstring
arriving + settled + departing. The complete value attributed to this object.
typescript
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 users
for (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).

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

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

typescript
const zone = await arca.createIsolationZone({
path: '/users/alice',
});
console.log(zone.boundaryId);
console.log(zone.isolation?.isBoundaryRoot); // true

getObjectVersions(objectId)

Get all versions of an Arca object at the same path (for deleted + recreated objects).

typescript
const { versions } = await arca.getObjectVersions(obj.id);

getSnapshotBalances(objectId, asOf)

Get historical balances and positions for an object at a specific timestamp.

typescript
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 OperationHandleawait 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.

typescript
// Simple deletion (no balance)
await arca.ensureDeleted({ ref: '/wallets/old' });
// Deletion with balance sweep
await arca.ensureDeleted({
ref: '/wallets/old',
sweepTo: '/wallets/main',
});
// Exchange deletion with position liquidation
await arca.ensureDeleted({
ref: '/exchanges/hl1',
sweepTo: '/wallets/main',
liquidatePositions: true,
});
refstringrequired
Full Arca path to delete.
sweepTostring
Arca path to sweep remaining funds into before deletion.

updateLabels(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.

typescript
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' }
objectIdstringrequired
Arca object ID.
labelsRecord<string, string | null>required
Label key-value pairs to set or remove. Keys must start with a letter (a-z/A-Z), contain only a-z, A-Z, 0-9, ., _, -, 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.

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

typescript
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)
pathstringrequired
Operation path (idempotency key). Must be unique per realm.
fromstringrequired
Source Arca object path to debit.
tostringrequired
Target Arca object path to credit.
amountstringrequired
Transfer amount as a decimal string.

estimateFee(params)

Estimate the fee for a transfer or order operation before executing it.

typescript
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'
actionstringrequired
Action type: transfer or order.
amountstringrequired
Amount as a decimal string.
sourcePathstring
Source Arca path (optional).
targetPathstring
Target Arca path (optional).

Static Fee Constants

The Arca.fees static property provides known fee constants for client-side calculations without a network call.

typescript
console.log(Arca.fees.exchangeTransfer); // '0.05'

fundAccount(opts)

Fund an Arca object (denominated or exchange). Returns an OperationHandleawait 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. Use handle.submitted to access the poolAddress before settlement.
typescript
// await waits for full settlement
await arca.fundAccount({
arcaRef: '/wallets/main',
amount: '1000.00',
});
// Or access the HTTP response before settlement
const handle = arca.fundAccount({ arcaRef: '/wallets/main', amount: '1000.00' });
const { poolAddress } = await handle.submitted;
await handle.wait({ timeout: 15000 });
arcaRefstringrequired
Target Arca object path.
amountstringrequired
Amount as a decimal string.

defundAccount(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().

typescript
await arca.defundAccount({
arcaPath: '/wallets/main',
amount: '500.00',
destinationAddress: '0xabc...',
});
arcaPathstringrequired
Source Arca object path.
amountstringrequired
Amount as a decimal string.
destinationAddressstring
On-chain destination address.

SDK: Operations & Events

getOperation(operationId, options?)

Get operation detail by ID, including correlated events and state deltas.

typescript
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 events
console.log(detail.deltas); // state deltas (before/after values)
includeEvidenceboolean
When true, includes Phase 1 audit evidence such as journal entries, journal proofs, chain transaction references, and integrity annotations.
typescript
const 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.

typescript
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.)
});
typeOperationType
Filter by a single type: transfer, create, delete, deposit, withdrawal, swap, order, fill, cancel.
typesOperationType[]
Filter by multiple types. Takes precedence over type.
includeContextboolean
When true, each operation includes its typed context inline (transfer amount/fee, fill details, etc.).

exportOperationEvidence(opts)

Export realm-scoped audit evidence over a time range, with optional operation-type and path filters.

typescript
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);
fromstringrequired
Required RFC 3339 start timestamp for the export window.
tostringrequired
Required RFC 3339 end timestamp for the export window.
typeOperationType
Optional single operation type filter.
typesOperationType[]
Optional multi-type filter. When supplied, it takes precedence over type.
arcaPathstring
Optional source or target Arca path prefix filter.
pathstring
Optional operation path prefix filter.
limitnumber
Optional page size. Defaults to 100; maximum 500.
cursorstring
Optional pagination cursor returned as nextCursor 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.

typescript
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 nonces
const { 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).

typescript
const { deltas, total } = await arca.listDeltas('/wallets/main');

summary()

Get aggregate counts for the realm.

typescript
const summary = await arca.summary();
console.log(summary.objectCount); // 12
console.log(summary.operationCount); // 47
console.log(summary.eventCount); // 93

Prefix vs. Exact Path

All aggregation methods accept a prefix parameter. A trailing slash aggregates all objects under that path (e.g., /users/alice/ returns all of Alice's objects). Omitting the trailing slash targets a single exact object (e.g., /users/alice/exchanges/hl1 returns only that object).

getPathAggregation(prefix, options?)

Get aggregated valuation for objects matching the prefix (or exact path).

typescript
// Aggregate all objects under a prefix
const agg = await arca.getPathAggregation('/users/alice/');
console.log(agg.totalEquityUsd, agg.breakdown);
// Single object
const single = await arca.getPathAggregation('/users/alice/exchanges/hl1');
// Historical aggregation at a past timestamp
const hist = await arca.getPathAggregation('/users/', { asOf: '2026-02-15T00:00:00Z' });

Response:

prefixstring
The prefix or path that was queried.
totalEquityUsdstring
Total equity in USD (2 decimal places).
departingUsdstring
Total departing (outbound held) balance in USD. Pass-through: already USD-denominated, not recomputed from mid prices.
arrivingUsdstring
Total arriving (inbound held) balance in USD. Pass-through: already USD-denominated, not recomputed from mid prices.
breakdownAssetBreakdown[]
Per-asset breakdown with amount, price, and USD value.
asOfstring?
Timestamp of the historical snapshot (omitted for live queries).

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.

typescript
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:

prefixstring
The prefix or path that was queried.
fromstring
Start of the time range (RFC 3339).
tostring
End of the time range (RFC 3339).
startingEquityUsdstring
Equity at the start of the range.
endingEquityUsdstring
Equity at the end of the range.
netInflowsUsdstring
Total inflows (deposits + inbound transfers) in USD.
netOutflowsUsdstring
Total outflows (outbound transfers) in USD.
pnlUsdstring
Calculated P&L: endingEquity - startingEquity - netInflows + netOutflows.
externalFlowsExternalFlowEntry[]
Itemized list of deposits and transfers that crossed the prefix boundary.

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.

typescript
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:

prefixstring
The prefix or path that was queried.
fromstring
Start of the time range (RFC 3339).
tostring
End of the time range (RFC 3339).
pointsnumber
Actual number of data points returned (default 200).
startingEquityUsdstring
Equity at the start of the range.
effectiveFromstring?
Timestamp of the first non-zero equity point. When the chart window starts before any balance exists, leading zero-equity points are trimmed and effectiveFrom indicates where the series actually begins. The SDK uses this internally to align flowsSince for watchPnlChart.
pnlPointsPnlPoint[]
Time-series array. Each entry has timestamp, pnlUsd, and equityUsd (strings, 2 decimal places).
externalFlowsExternalFlowEntry[]
Itemized deposits and transfers that crossed the prefix boundary.

getEquityHistory(prefix, from, to, points?)

Get equity time-series sampled evenly over a time range (default 200 points, max 1000).

typescript
const history = await arca.getEquityHistory('/users/', '2026-02-01T00:00:00Z', '2026-03-01T00:00:00Z', 100);

Response:

prefixstring
The prefix or path that was queried.
fromstring
Start of the time range.
tostring
End of the time range.
pointsnumber
Actual number of data points returned.
equityPointsEquityPoint[]
Time-series array. Each entry has 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.

typescript
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
Object path or path prefix (trailing / for prefix).
fromstringrequired
Start of the time range (RFC 3339).
tostringrequired
End of the time range (RFC 3339).
pointsnumber
Number of historical data points (default 200).
options.exchangestring
Exchange filter (default "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.

typescript
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 reference
console.log('Starting equity:', chart.startingEquityUsd);
chart.close();
pathstringrequired
Object path or path prefix (trailing / for prefix).
fromstringrequired
Start of the time range (RFC 3339).
tostringrequired
End of the time range (RFC 3339).
pointsnumber
Number of historical data points (default 200).
options.exchangestring
Exchange filter (default "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.
typescript
// 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.

typescript
// Track all objects under a user prefix
const { watchId, aggregation } = await arca.createAggregationWatch([
{ type: 'prefix', value: '/users/alice/' },
]);
console.log(aggregation.totalEquityUsd); // e.g. "15734.56"
console.log(aggregation.breakdown); // per-asset rollup
sourcesAggregationSourceDto[]required
Array of source selectors. Each source has a type and a type-specific field (value for prefix/pattern/watch, paths for paths). Multiple sources are unioned together.

Source Types

TypeValueMatches
prefixPath prefix with trailing slashAll objects whose path starts with the prefix (e.g., /users/alice/ matches /users/alice/main, /users/alice/exchanges/hl1)
patternGlob with * wildcardSingle-segment wildcard matching (e.g., /users/*/exchanges/hl/* matches all users' Hyperliquid exchanges)
pathsArray of paths (via paths field)Exact path list (e.g., ['/treasury/usd', '/treasury/btc'])
watchWatch IDCompose another watch — includes all objects tracked by the referenced watch
typescript
// Combine multiple source types in a single watch
const { 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.

typescript
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 done
stream.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.

typescript
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 updates
arca.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 values
const prices = await arca.watchPrices();
prices.onUpdate((mids) => {
portfolio = revalueAggregation(portfolio, mids);
renderPortfolio(portfolio);
});
// 4. Clean up when done
prices.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.

typescript
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).

typescript
await arca.destroyAggregationWatch(watchId);

Response shape (PathAggregation):

prefixstring
The prefix or "(watch)" for watch-based aggregation.
totalEquityUsdstring
Total equity in USD across all matched objects.
departingUsdstring
Total departing (outbound held) balance in USD. Pass-through (not price-dependent).
arrivingUsdstring
Total arriving (inbound held) balance in USD. Pass-through (not price-dependent).
breakdownAssetBreakdown[]
Per-asset breakdown with amount, price, and USD value.

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

typescript
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).

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

Exchange Setup SequenceCreateensurePerpsExchangeFundtransfer / fundAccountLeverageupdateLeverageTradeplaceOrderWatchwatchFillsEach step is an idempotent operation. Steps 1-3 are one-time setup; step 4 repeats per trade.

ensurePerpsExchange(opts)

Ensure a perps exchange Arca object exists at the given path. Denomination is automatically set to USD.

typescript
const { object } = await arca.ensurePerpsExchange({
ref: '/exchanges/hl1',
exchangeType: 'hyperliquid', // default
});
refstringrequired
Full Arca path for the exchange object.
exchangeTypestring
Exchange provider. Currently only hyperliquid.
operationPathstring
Optional idempotency key.

getExchangeState(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.

typescript
const state: ExchangeState = await arca.getExchangeState(objectId);
console.log(state.marginSummary.equity); // equity
console.log(state.marginSummary.initialMarginUsed); // initial margin in use
console.log(state.marginSummary.maintenanceMarginRequired); // liquidation threshold
console.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.

equitystring
Total account value: deposited funds + unrealized PnL.
totalRawUsdstring
Total deposited USD before PnL (your "wallet balance").
availableToWithdrawstring
Withdrawable amount: equity minus maintenance margin, floored at zero. This is how much can be withdrawn without hitting liquidation — it is not free margin for new trades. Use equity - initialMarginUsed for available trading margin, or watchMaxOrderSize() for per-coin max sizes.
initialMarginUsedstring
Total margin locked in open positions.
maintenanceMarginRequiredstring
Minimum margin required before liquidation.
totalUnrealizedPnlstring
Sum of unrealized PnL across all positions.
totalNtlPosstring
Sum of absolute notional position sizes.

updateLeverage(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.

typescript
await arca.updateLeverage({
objectId: exchangeId,
coin: 'hl:BTC',
leverage: 10,
});
objectIdstringrequired
Exchange Arca object ID.
coinstringrequired
Canonical market ID of the coin to set leverage for (e.g. hl:BTC).
leveragenumberrequired
Leverage multiplier (1 to maxLeverage).

placeOrder(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.

typescript
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 filled
const filled = await order.filled({ timeout: 30000 });
console.log(filled.fills);
// Stream fills via async iterator
for 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 order
const 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.

typescript
// Get the current max from the live stream
const maxStream = await arca.watchMaxOrderSize({
objectId: exchangeId,
coin: 'hl:BTC',
side: 'BUY',
leverage: 5,
});
const displayedMax = maxStream.value.activeAssetData.maxBuySize;
// Place with useMax — server resolves atomically
const 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:

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

typescript
// 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 position
await arca.closePosition({
path: '/op/close/btc-partial',
objectId: exchangeId,
coin: 'hl:BTC',
size: '0.005',
});
pathstringrequired
Operation path (idempotency key).
objectIdstringrequired
Exchange Arca object ID.
coinstringrequired
Coin to close (e.g. hl:BTC).
sizestring
Partial close size. Omit to close the full position.
timeInForcestring
Time in force (default: IOC).

If you need more control (limit price, custom side), use placeOrder directly with reduceOnly: true:

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

pathstringrequired
Operation path (idempotency key).
objectIdstringrequired
Exchange Arca object ID.
coinstringrequired
Canonical market ID of the coin to trade (e.g., hl:BTC, hl:ETH).
side'BUY' | 'SELL'required
Order side.
orderType'MARKET' | 'LIMIT'required
Order type.
sizestringrequired
Order size as a decimal string.
pricestring
Limit price. Required for LIMIT orders.
leveragenumber
Optional leverage override. When provided, sets the account's per-coin leverage before placing the order and persists for subsequent orders on this coin. Omit to use the current setting.
reduceOnlyboolean
Only reduce an existing position. Default: false.
timeInForcestring
Time in force: GTC (default), IOC, ALO.
isTriggerboolean
If true, place a trigger order (TP/SL). When set, triggerPx and tpsl are required.
triggerPxstring
Mark price threshold that activates the order.
isMarketboolean
If true, execute as market when triggered; if false, use price as the limit price after trigger.
tpsl'tp' | 'sl'
Take profit (tp) or stop loss (sl). Required when isTrigger is true.
grouping'na' | 'normalTpsl' | 'positionTpsl'
Lifecycle grouping: 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.

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

typescript
const { order, fills }: SimOrderWithFills = await arca.getOrder(exchangeId, orderId);
console.log(order.status); // 'FILLED'
console.log(fills.length); // number of fills

cancelOrder(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).

typescript
await arca.cancelOrder({
path: '/op/cancel/btc-1',
objectId: exchangeId,
orderId: orderId,
});
pathstringrequired
Operation path (idempotency key).
objectIdstringrequired
Exchange Arca object ID.
orderIdstringrequired
ID of the order to cancel.

placeTwap(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.

typescript
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'
exchangeIdstringrequired
Exchange Arca object ID.
pathstringrequired
Operation path (idempotency key).
coinstringrequired
Canonical coin identifier (e.g. "hl:BTC").
side'BUY' | 'SELL'required
Order side.
totalSizestringrequired
Total size to execute over the duration.
durationMinutesnumberrequired
Duration in minutes (1 to 43200 = 30 days).
intervalSecondsnumber
Interval between slices in seconds (10 to 3600, default 30).
randomizeboolean
Add timing jitter to slice placement (default false).
reduceOnlyboolean
Reduce-only mode (default false).
leveragenumber
Leverage multiplier.
slippageBpsnumber
Max slippage in basis points (10 to 1000, default 300 = 3%).

cancelTwap(exchangeId, operationId)

Cancel an active TWAP. Returns an OperationHandle.

typescript
await arca.cancelTwap(exchangeId, operationId);

getTwap(exchangeId, operationId)

Get TWAP status and progress by its parent operation ID.

typescript
const { twap } = await arca.getTwap(exchangeId, operationId);
console.log(twap.executedSize, twap.filledSlices, twap.status);

listTwaps(exchangeId, activeOnly?)

List TWAPs for an exchange object.

typescript
const twaps = await arca.listTwaps(exchangeId, true); // active only

getTwapLimits()

Get TWAP limits and constraints for validation before placing a TWAP. Returns static platform limits.

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

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

typescript
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
}
marketstring
Filter by market coin (e.g. hl:BTC).
startTimestring
RFC 3339 timestamp — fills on or after.
endTimestring
RFC 3339 timestamp — fills on or before.
limitnumber
Max fills to return (default 100, max 500).
cursorstring
Pagination cursor (createdAt of last fill).

watchFills(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.

typescript
const stream = await arca.watchFills(exchangeId, { market: 'hl:BTC' });
// Initial fills are available immediately
console.log(stream.fills);
// Live updates as new fills arrive
stream.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 done
stream.close();
marketstring
Filter by market coin (e.g. hl:BTC).
limitnumber
Max fills for initial fetch (default 100, max 500).

tradeSummary(objectId, opts?)

Get per-market P&L aggregation: total realized P&L, total fees, trade count, and volume, with cross-market totals.

typescript
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);
startTimestring
RFC 3339 timestamp — scope summary start.
endTimestring
RFC 3339 timestamp — scope summary end.

Funding 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:

typescript
// Callback-based funding listener
const 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 directly
arca.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.

typescript
// Single coin
const setting = await arca.getLeverage(exchangeId, 'hl:BTC');
// All coins
const 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.

typescript
const data = await arca.getActiveAssetData(exchangeId, 'hl:BTC');
// Use maxBuySize / maxSellSize for order size sliders
const maxLong = parseFloat(data.maxBuySize);
const maxShort = parseFloat(data.maxSellSize);
// Pass leverage to override the stored setting
const data10x = await arca.getActiveAssetData(exchangeId, 'hl:BTC', 0, 10);
coinstring
Canonical market ID (e.g. hl:BTC).
leverage{'{'}type: LeverageType, value: number{'}'}
Current leverage setting. type is "cross" or "isolated".
maxBuySizestring
Max buy size in tokens (positive). Accounts for margin, leverage, and existing positions.
maxSellSizestring
Max sell size in tokens (positive). Accounts for margin, leverage, and existing positions.
availableToTradestring
Raw available margin in USD (equity minus margin in use). Direction-agnostic — use for "buying power" display. For per-side max exposure, use maxBuyUsd/maxSellUsd.
maxBuyUsdstring
Max buy size in USD (positive).
maxSellUsdstring
Max sell size in USD (positive).
markPxstring
Current mark price as a decimal string.
feeRatestring
All-in fee rate as a decimal (e.g. "0.00045" for 4.5 bps). Includes exchange taker fee (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.

typescript
// "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.

typescript
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
Canonical market ID (e.g. "hl:BTC").
takerFeeRatestring
Effective taker fee rate as a decimal (e.g. "0.00045" = 4.5 bps).
makerFeeRatestring
Effective maker fee rate as a decimal (e.g. "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.

typescript
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
Canonical market identifier (e.g. "hl:BTC", "hl:ETH", or "xyz:TSLA" for HIP-3 DEX assets).
symbolstring
Base asset symbol without exchange prefix (e.g. "BTC", "ETH").
exchangestring
Exchange identifier (e.g. "hl" for Hyperliquid).
dexstring?
HIP-3 DEX name, if the asset belongs to a builder-deployed DEX. Omitted for native Hyperliquid perps.
szDecimalsnumber
Number of decimal places for order size precision. For example, szDecimals: 4 means sizes must be multiples of 0.0001. Truncate or round order sizes to this precision before submitting.
maxLeveragenumber
Maximum leverage multiplier allowed for this asset. Varies by asset (e.g. BTC may allow 40x while smaller assets allow 50x). The SDK rejects orders with leverage exceeding this value.
onlyIsolatedboolean
If true, only isolated margin is supported — cross-margin is not available for this asset.
feeScalenumber?
HIP-3 fee multiplier. 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?
Candle history availability. Contains earliestMs (absolute earliest candle timestamp, including extended pre-listing data) and hlEarliestMs (earliest venue-native candle timestamp). When earliestMs < hlEarliestMs, extended history is available. Use earliestMs as startTime for a “Max” chart button. Omitted when bounds are not yet computed.
displayNamestring?
Human-readable name for the asset (e.g. "Bitcoin", "Crude Oil", "S&P 500"). Use for UI labels; fall back to symbol when absent. Curated via the admin panel.
logoUrlstring?
CDN URL for the asset logo image. Use for asset icons in UI. Omitted when no curated logo is available. Managed via the admin panel.
isHip3boolean?
Whether this asset is a HIP-3 deployer market. Use to distinguish native perps from builder-deployed markets in asset pickers.
deployerDisplayNamestring?
Full display name of the HIP-3 deployer. Omitted for native perps.

Other Market Endpoints

typescript
// 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 book
const 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.

typescript
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
Exchange identifier. Default: "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.

typescript
import { Arca } from '@arcaresearch/sdk';
const arca = new Arca({ apiKey: 'ak_...', realm: 'my-realm' });
await arca.ready();
// 1. Create a wallet to hold funds
const { nonce: n1 } = await arca.nonce('/demo/wallet');
const wallet = await arca.ensureDenominatedArca({ path: '/demo/wallet', nonce: n1 });
// 2. Create an exchange account
const { nonce: n2 } = await arca.nonce('/demo/exchange');
const exchange = await arca.ensurePerpsExchange({ ref: '/demo/exchange', nonce: n2 });
// 3. Fund the wallet and transfer to exchange
await 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 order
await 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 updates
const stream = await arca.watchExchangeState(exchange.arcaId);
stream.onUpdate((state) => console.log('Equity:', state.equity));
// 6. Close the position
await arca.closePosition({ objectId: exchange.arcaId, coin: 'hl:BTC' });
// 7. Transfer remaining balance back and clean up
const 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.

typescript
const stream = await arca.watchPrices();
console.log(stream.prices); // { 'hl:BTC': '95432.50', 'hl:ETH': '3210.10' }
stream.onUpdate((prices) => renderAssetList(prices));
// Async iteration
for await (const prices of stream) {
console.log(prices);
}
stream.close(); // release the subscription

watchOperations()

Subscribe to real-time operation creates and updates. Returns an OperationWatchStream.

typescript
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?)

Recommended for live balance display

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.

typescript
// Watch all balances in the realm
const stream = await arca.watchBalances();
// Or filter to a specific path prefix
const stream = await arca.watchBalances('/wallets/main');
// Initial snapshot is available immediately after await
console.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 change
stream.onUpdate(({ entityId, entityPath, balances, event }) => {
console.log(`Balance changed on ${entityPath}`, balances);
// balances: [{ id: '...', arcaId: '...', denomination: 'USD', amount: '1250.00' }]
});
// Async iteration also works
for await (const update of stream) {
updateUI(update.entityPath, update.balances);
if (done) break;
}
stream.close();
arcaRefstring
Optional path prefix filter. When provided, only balance events for objects whose path starts with this prefix are delivered. Omit to receive all balance events in the realm.

Types:

typescript
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).

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

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

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

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

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

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

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

typescript
const stream = await arca.watchExchangeState(exchangeId);
// Initial state is available immediately
console.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.

typescript
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 destination
balanceStream.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.

typescript
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.operations
opStream.trackSubmission(handle);

Auto-Tracking

For the simplest setup, use enableAutoTracking() to automatically apply predictions and track submissions on every mutation:

typescript
const opStream = await arca.watchOperations();
const balanceStream = await arca.watchBalances();
const exchangeStream = await arca.watchExchangeState(exchangeId);
// One line — all future mutations get automatic optimistic feedback
arca.enableAutoTracking(opStream, balanceStream, exchangeStream);
// Now every transfer, placeOrder, closePosition automatically shows
// immediate feedback in the registered streams
const handle = arca.transfer({ ... });
// balanceStream immediately shows departing amount
// opStream immediately shows pending operation

Stream API

All watch streams share these capabilities:

Property / MethodDescription
.onUpdate(cb)Register a callback for each update. Returns an unsubscribe function.
.close()Stop listening and release the underlying subscription.
.isClosedWhether 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.

bash
npm install @arcaresearch/react
typescript
import { 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:

typescript
// 1. Always close in a finally block
const 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

ClassCodeHTTP StatusWhen
ValidationErrorVALIDATION_ERROR400Invalid input
UnauthorizedErrorUNAUTHENTICATED401Missing or invalid auth
NotFoundErrorNOT_FOUND404Resource not found
ConflictErrorCONFLICT409Duplicate or invalid state
InternalErrorINTERNAL_ERROR500Unexpected server error

Usage Pattern

typescript
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

typescript
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

typescript
const admin = new ArcaAdmin({ baseUrl: 'https://api.arcaos.io' });
const { token, refreshToken, expiresAt } = await admin.refresh(oldRefreshToken);
// Use the new token for subsequent requests
admin.setToken(token);

Logout

typescript
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 min

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

typescript
const { token, refreshToken, builder } = await admin.signInWithGoogle(
googleIdToken,
'My Org', // optional — required for new accounts
);
googleIdTokenstringrequired
Google ID token from the OAuth flow.
orgNamestring
Organization name. Required when the user is new. Omit for existing users.

SDK: 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.

typescript
const { organization, membership } = await admin.createOrg('Acme Inc');
console.log(organization.id, organization.slug);
console.log(membership.role); // 'owner'
namestringrequired
Organization name.

Get Organization

typescript
const admin = new ArcaAdmin({ token });
const org = await admin.getOrg();
console.log(org.name, org.slug);

List Members

typescript
const { members, total } = await admin.listMembers();
for (const m of members) {
console.log(m.email, m.role, m.realmTypeScope);
}

Invite a Member

typescript
const invitation = await admin.inviteMember({
email: 'alice@example.com',
role: 'developer',
realmTypeScope: ['development', 'staging'],
});
console.log(invitation.inviteLink);

Update Member Role

typescript
await admin.updateMember('member-id', {
role: 'admin',
realmTypeScope: null, // null = all realm types
});

Remove Member

typescript
await admin.removeMember('member-id');

Transfer Ownership

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

typescript
const status = await arca.getCustodyStatus();
console.log(status.contractAddress, status.totalBalance);
console.log(status.boundaries);
console.log(status.exchangeArcas);

Get Boundary

typescript
const boundary = await arca.getBoundary('b0');
console.log(boundary.balance, boundary.status, boundary.recoveryKey);
boundaryIdstringrequired
The isolation boundary identifier.

List Boundaries

typescript
const boundaries = await arca.listBoundaries();
for (const b of boundaries) {
console.log(b.id, b.balance, b.status);
}

List Exchange Arcas

typescript
// All exchange arcas
const arcas = await arca.listExchangeArcas();
// Filtered to a specific boundary
const filtered = await arca.listExchangeArcas('b0');
boundaryIdstring
Optional boundary ID to filter exchange arcas.

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

typescript
// User pastes or connects an existing wallet address
await 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.

typescript
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

PlatformPath A (existing wallet)Path B (generated mnemonic)
iOSWalletConnect or paste addressStore in Keychain with kSecAttrSynchronizable (syncs via iCloud)
AndroidWalletConnect or paste addressStore in EncryptedSharedPreferences with Google backup
WebWalletConnect, 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.

boundaryIdstringrequired
The boundary to assign the recovery key to.
walletAddressstringrequired
The Ethereum address of the recovery key (from an existing wallet or from Arca.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.

typescript
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 transfer
const 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.