Arca/Documentation
Join Waitlist

SDK: Installation & Setup

The @arca-network/sdk package provides a high-level, typed, idempotent interface for building on Arca. It requires Node.js 20+ and is written in TypeScript (ES2022).

Installation

bash
npm install @arca-network/sdk

Package Exports

The package exports one main class and all supporting types:

typescript
import { Arca } from '@arca-network/sdk';
// Types are also exported for strong typing
import type {
ArcaObject,
ArcaBalance,
Operation,
TransferOptions,
PlaceOrderOptions,
RealmEvent,
// ... 70+ exported types
} from '@arca-network/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 '@arca-network/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, but they bypass stale cache entries after WebSocket reconnects, delivery-sequence gaps, or chart snapshot updates.

By default, the cache holds up to 50 entries and each entry expires after 5 minutes. You can customize both via the cache config option. Lower ttlMs for stronger freshness, or set it to 0 for no expiry (entries then live until LRU eviction):

typescript
// Custom cache size and TTL
const arca = new Arca({
apiKey: 'arca_78ae7276_...',
realm: 'development',
cache: { maxEntries: 100, ttlMs: 60_000 },
});
// 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 '@arca-network/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,
market: 'hl:0:BTC', side: 'buy', orderType: 'MARKET', size: '0.01',
});
const submitted = await order.submitted; // HTTP acknowledgement before settlement
await order; // wait for placement settlement
// 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?.direction, 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();
// Resize the order (auto-generates a per-resize path that encodes newSize).
// Only sized orders resize: limit orders and sized TP/SL triggers. Unsized
// (sizeToMax: true) triggers are rejected — they always close the whole position.
const resizeResult = await order.resize("0.75");

For backend wrappers around order submission, use a stable operation path per logical order attempt and await order.submitted for the HTTP acknowledgement. Awaiting the handle waits for settlement and can time out even after Arca has accepted the order.

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
order.resize(newSize)OrderHandleResize a sized order (limit / sized TP/SL); unsized triggers rejected

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). Safe to call on every request — eliminates the need for separate create-then-get patterns. Denominated and exchange objects are USD-denominated; the field is server-stamped and not a request input. 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',
operationPath: '/op/create/wallets/main:1', // optional idempotency key
});
refstringrequired
Full Arca path.
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',
});
refstringrequired
Full Arca path.
typestringrequired
One of: denominated, exchange, deposit, withdrawal, escrow.
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
console.log(detail.object.boundary); // recovery-hatch state, when frozen

boundary is present only when the object's isolation boundary has been frozen by an on-chain recovery action. Two states: soft_frozen (precautionary lock by the recovery key holder, reversible) and hard_frozen (a Withdrawn event has fired; the funds are no longer the platform's to spend, and user wallets in this boundary have been swept into a system-owned recovery arca at boundary.recoveryArcaPath). Builders should treat any non-active boundary as a freeze — render a banner and disable mutating actions on the object.

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.

Both objects and paths are returned newest-first (createdAt descending) with the entry path as a stable tiebreaker. The legacy folders string array follows the same order as paths.

typescript
const { folders, paths, currentIsolation, objects } = await arca.browseObjects({
prefix: '/users/',
includeDeleted: false,
});
for (const entry of paths ?? []) {
// entry.createdAt is the folder's effective creation timestamp:
// for isolation zones, the zone's own created_at; for plain folders,
// MIN(descendant.createdAt) over the arcas the browse query returned.
// entry.labels carries folder-level labels stored separately from
// arca objects — set with updateFolderLabels().
console.log(entry.path, entry.kind, entry.createdAt, entry.labels);
}

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.

getFolderLabels(path)

Read folder-level labels for a path. Folders are virtual aggregations of arca paths (see browseObjects()); their labels live independently in a dedicated folder_labels store, surfaced inline on each BrowsePathEntry. Returns an empty labels map when no labels are set.

typescript
const { labels, createdAt, updatedAt } = await arca.getFolderLabels('/users/alice');
console.log(labels); // { name: 'Alice', team: 'alpha' }
pathstringrequired
Folder path. Must be absolute and non-root.

updateFolderLabels(path, labels)

Replace the entire folder label set with the provided map. Unlike updateLabels(), this is a full overwrite — pass the desired final state, not a delta. Pass an empty map ({}) to clear all labels on the folder.

typescript
await arca.updateFolderLabels({
path: '/users/alice',
labels: { name: 'Alice', team: 'alpha' },
});
// Clear all labels:
await arca.updateFolderLabels({ path: '/users/alice', labels: {} });
pathstringrequired
Folder path. Must be absolute and non-root.
labelsRecord<string, string>required
Full label set. Same validation rules as object labels (32 keys max, key pattern ^[a-zA-Z][a-zA-Z0-9._-]*$, arca. and _ reserved prefixes, value ≤ 256 characters). Requires the arca:UpdateFolderLabels permission on the folder path.

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.
feeOverridestring
Override the transfer fee for this operation. Set to "0" to disable the fee, or any non-negative decimal for a custom fee. Non-production realms only.

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' — the network takes no transfer fee

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 Asset

fundAccount behavior depends on the realm's asset tier (paper vs live):

  • paper (legacy development) — auto-mints tokens to the realm's custody pool and settles automatically within seconds. No real tokens or wallet needed.
  • live (legacy 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, operation path, object path, or exact object ID.

typescript
const { operations, total } = await arca.listOperations({
type: 'transfer', // single type filter
types: ['fill', 'transfer'], // or multiple types (takes precedence)
arcaId: wallet.id, // exact object history
includeContext: true, // inline context (amount, fee, etc.)
});
typeOperationType
Filter by a single type: transfer, create, delete, deposit, withdrawal, swap, order, fill, cancel, fee_distribution, adjustment, funding, venue_close, or twap.
typesOperationType[]
Filter by multiple types. Takes precedence over type.
arcaIdstring
Filter by exact source or target Arca object ID. Prefer this for a single object's history; arcaPath is path-prefix scoped.
arcaPathstring
Filter by source or target Arca path prefix.
pathstring
Filter by operation path prefix.
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.
arcaIdstring
Optional exact source or target Arca object ID 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?, options?)

Returns P&L time-series data for objects under a path prefix, adjusted for external flows (deposits/withdrawals). Each point includes pnlUsd and raw equityUsd. Pass { anchor: "equity" } to also receive flow-adjusted valueUsd, where transfers are applied to the baseline instead of shown as timeline jumps.

typescript
const history = await arca.getPnlHistory(
'/users/alice/',
'2026-02-01T00:00:00Z',
'2026-03-01T00:00:00Z',
100,
{ anchor: 'equity' },
);
console.log(history.pnlPoints[0].pnlUsd, history.pnlPoints[0].valueUsd);

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 1000, max 1000).
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.
resolutionstring?
Actual sampling resolution returned by the V2 history service.
resolutionRequestedstring?
Present when the service promoted the request to a coarser retained resolution.
serverNowstring?
Server timestamp used to classify the current open bucket.
pnlPointsPnlPoint[]
Time-series array. Each entry has timestamp, pnlUsd, equityUsd, and optional V2 status metadata (open, sealed, carried, incomplete).
externalFlowsExternalFlowEntry[]
Itemized deposits and transfers that crossed the prefix boundary.
options.anchorstring
"zero" (default) for standard P&L, or "equity" to include flow-adjusted valueUsd on each point.
options.kindstring
"path" (default) aggregates across every object under the prefix. "object" charts a single object by id and lets the server skip enumerating the prefix — pass objectId with it.
options.objectIdstring?
Required when kind is "object". The object's id (obj_…).

getEquityHistory(prefix, from, to, points?, options?)

Get equity time-series sampled evenly over a time range (default 1000 points, max 1000). The backend ladder picks the finest resolution that fits inside points; the default targets 5m for 24h, 1h for 1M, and 4h for 3M ranges. Pass { kind: 'object', objectId } to chart a single object by id (the server then skips prefix enumeration).

typescript
const history = await arca.getEquityHistory('/users/', '2026-02-01T00:00:00Z', '2026-03-01T00:00:00Z', 100);
// Single object by id (skips prefix enumeration on the server):
const one = await arca.getEquityHistory(
'/users/alice/main', '2026-02-01T00:00:00Z', '2026-03-01T00:00:00Z', 100,
{ kind: 'object', objectId: 'obj_...' },
);

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.
resolutionstring?
Actual sampling resolution returned by the V2 history service.
resolutionRequestedstring?
Present when the service promoted the request to a coarser retained resolution.
serverNowstring?
Server timestamp used to classify the current open bucket.
equityPointsEquityPoint[]
Time-series array. Each entry has timestamp, equityUsd, and optional V2 status metadata (open, sealed, carried, incomplete).

watchEquityChart(path, from, to, points?, options?)

Create a live equity chart that merges V2 history with real-time chart snapshot recovery and aggregation updates. The rightmost point is treated as the current open bucket and updates as equity changes; after reconnects, app backgrounding, or delivery gaps, the SDK refetches V2 history so sealed buckets converge to the server's authoritative rows. 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 1000, max 1000).
options.exchangestring
Exchange filter (default "sim").

watchPnlChart(path, from, to, points?, options?)

Create a live P&L chart that merges V2 P&L history with real-time chart snapshot recovery and aggregation updates. The rightmost point tracks the current open bucket; reconnects and delivery gaps trigger a V2 history refetch so missed sealed buckets are filled from the server rather than synthesized locally. Deposits, withdrawals, and transfers that cross the path boundary are tracked as external flows and subtracted from equity changes, so P&L reflects 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 1000, max 1000).
options.exchangestring
Exchange filter (default "sim").
options.anchorstring
"zero" (default) for standard P&L starting at 0, or "equity" to map historical points to their actual historical equityUsd and the live point to the current account equity. When set to "equity", each point includes a valueUsd field suitable for the chart y-axis. This provides a true historical portfolio value view.
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 '@arca-network/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 '@arca-network/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',
venue: 'hl-sim', // default — simulated Hyperliquid
});
refstringrequired
Full Arca path for the exchange object.
venuestring
Venue the exchange object trades against: hl-sim (default — a simulated Hyperliquid account for paper realms) or hl (a live Hyperliquid account for production realms).
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.

Position Cost Accumulators

Each SimPosition in state.positions carries running totals of what the account has paid (or earned) to hold the position. These accumulate over the position's current open lot — the period from when net position size last went 0 → non-zero up to now — and reset when the lot ends. A full close removes the position row entirely; a flip-through-zero starts a new lot at the opening-portion's proportional share of the flip-fill's fees. Fields are omitted on positions with no accrued cost so consumers can distinguish "not yet computed" from a placeholder zero.

cumulativeFundingstring
Funding paid (negative) or received (positive) over the current open lot. Decimal string.
cumulativeFeestring
Total trading fee paid over the current open lot. Equal to cumulativeExchangeFee + cumulativePlatformFee + cumulativeBuilderFee by construction. Decimal string.
cumulativeExchangeFeestring
Exchange (taker / maker) fee component of cumulativeFee.
cumulativePlatformFeestring
Platform fee component of cumulativeFee.
cumulativeBuilderFeestring
Builder fee component of cumulativeFee.

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,
market: 'hl:0:BTC',
leverage: 10,
});
objectIdstringrequired
Exchange Arca object ID.
marketstringrequired
Canonical market ID to set leverage for (e.g. hl:0:BTC).
leveragenumberrequired
Leverage multiplier (1 to maxLeverage).

updateIsolatedMargin(opts)

Add or remove collateral from an isolated-margin position. Isolated positions carry their own dedicated collateral and are liquidated independently of the cross pool. A positiveamount moves USD from the account's free balance into the position (lowering its liquidation price); a negative amount removes collateral (raising it). Removal is rejected if it would drop the position below its maintenance margin. Only valid on isolated positions.

typescript
await arca.updateIsolatedMargin({
objectId: exchangeId,
market: 'hl:1:CL',
amount: '25', // add $25; use a negative string to remove
});
objectIdstringrequired
Exchange Arca object ID.
marketstringrequired
Canonical market ID of the isolated position (e.g. hl:1:CL).
amountstringrequired
Signed USD amount as a decimal string: positive adds, negative removes.

setMarginMode(opts)

Switch an asset between cross and isolated margin for an exchange Arca object. Rejected on isolated-only (HIP-3) markets and while an open position exists for the asset — close the position first. Leverage is remembered per mode (matching Hyperliquid): switching modes restores the leverage you last set for that mode rather than carrying one value across.

typescript
await arca.setMarginMode({
objectId: exchangeId,
market: 'hl:0:BTC',
marginMode: 'isolated',
});
objectIdstringrequired
Exchange Arca object ID.
marketstringrequired
Canonical market ID (e.g. hl:0:BTC).
marginMode'cross' | 'isolated'required
Margin mode to set for the asset.

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. Reduce-only orders never reconfigure stored leverage — for a reduceOnly order leverage is optional and treated as a no-op (the close uses the position's current leverage), so you can omit it even on isolated-only markets.

Isolated margin. Isolated-only markets — those whose marginModes is ['isolated'] (e.g. hl:1:CL) — reject cross orders at Hyperliquid's matching engine. Pass isolated: true together with a positive leverage when opening or increasing on those markets; a reduceOnly close/trim is accepted without leverage (it uses the position's current leverage). The platform rejects cross orders on isolated-only markets with a 400 before the operation is created. closePosition() handles this automatically; you only need to set isolated yourself when calling placeOrder directly. Read Market.marginModes via arca.market(id) to discover which margin modes a market supports — do not infer from isHip3, since some HIP-3 markets (e.g. hl:1:TSLA) are cross-eligible.

typescript
const order = arca.placeOrder({
path: '/op/order/btc-buy-1',
objectId: exchangeId,
market: 'hl:0:BTC',
side: 'buy',
orderType: 'MARKET',
size: '0.01',
leverage: 5,
});
const submitted = await order.submitted; // HTTP acknowledgement before settlement
await order; // wait for placement settlement
// 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,
market: 'hl:0: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,
market: 'hl:0: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.

The helper also auto-fills leverage and isolatedfor isolated-only markets such as hl:1:CL: Hyperliquid's matching engine buckets isolated positions by leverage, so a close must carry both fields to identify the bucket. Pass isolated or leverage to override the inference.

typescript
// Close a full BTC position (side + size inferred)
await arca.closePosition({
path: '/op/close/btc-1',
objectId: exchangeId,
market: 'hl:0:BTC',
});
// Partial close — close 0.005 of a BTC position
await arca.closePosition({
path: '/op/close/btc-partial',
objectId: exchangeId,
market: 'hl:0:BTC',
size: '0.005',
});
// Isolated-only close — leverage and isolated are auto-filled from the
// position and the market's marginModes. The SDK reads the position's
// leverage and looks up Market.marginModes for you.
await arca.closePosition({
path: '/op/close/cl-1',
objectId: exchangeId,
market: 'hl:1:CL',
});
pathstringrequired
Operation path (idempotency key).
objectIdstringrequired
Exchange Arca object ID.
marketstringrequired
Market to close (e.g. hl:0:BTC).
sizestring
Partial close size. Omit to close the full position.
timeInForcestring
Time in force (default: IOC).
isolatedboolean
Override the inferred isolated flag. Defaults to isolated-only from market meta (marginModes is ['isolated']).
leveragenumber
Override the inferred leverage. Defaults to the position's leverage.

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,
market: 'hl:0: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.
marketstringrequired
Canonical market ID to trade (e.g., hl:0:BTC, hl:0:ETH).
side'buy' | 'sell'required
Order side.
orderType'MARKET' | 'LIMIT'required
Order type.
sizestringrequired
Order size as a decimal string in base-asset units (e.g. BTC for hl:0:BTC), not USD. Hyperliquid enforces a $10 minimum on size × price; reduce-only orders and unsized (sizeToMax: true) triggers are exempt. Ignored when sizeToMax is true.
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.
sizeToMaxboolean
Marks an unsized ("size to max") TP/SL: it carries no fixed quantity and closes the entire position when triggered, regardless of size. Leave unset (the default) for a sized TP/SL that closes its fixed size (reduce-only). Either way, no TP/SL outlives the position — all waiting triggers for the market are cancelled when the position reaches zero.

Trigger orders surface as WAITING_FOR_TRIGGER and TRIGGERED in addition to standard order statuses.

Position TP/SL (ergonomic)

Rather than hand-building a trigger order, attach a stop-loss or take-profit to an existing position with setStopLoss() / setTakeProfit(). Each looks up the open position, infers the closing side (a LONG is protected by a SELL trigger, a SHORT by a BUY), and by default places a reduce-only unsized (sizeToMax: true) order — so when it fires it closes the entire live position regardless of size, and it is cancelled when the position closes or is liquidated. To scale out only part of a position, pass a base-unit size: the leg becomes a sized reduce-only partial that closes exactly that quantity (capped at the live position; min-notional waived). Leverage and isolated are auto-filled from the position and market meta. Both return an OrderHandle; there must be an open position for coin or the call throws NotFoundError.

typescript
// Stop-loss: a LONG BTC position gets a reduce-only SELL trigger at 54,000
await arca.setStopLoss({
path: '/op/tpsl/btc-sl',
objectId: exchangeId,
market: 'hl:0:BTC',
triggerPx: '54000',
}).submitted;
// Take-profit (same shape; tpsl: 'tp' under the hood)
await arca.setTakeProfit({
path: '/op/tpsl/btc-tp',
objectId: exchangeId,
market: 'hl:0:BTC',
triggerPx: '72000',
}).submitted;

By default a new trigger replaces any existing position TP/SL of the same type for the coin (cancel-then-place); pass replace: false to stack. Pass isMarket: false with a limitPrice for a trigger-limit instead of a market trigger.

pathstringrequired
Operation path (idempotency key).
objectIdstringrequired
Exchange Arca object ID.
marketstringrequired
Market of the open position (e.g. hl:0:BTC).
triggerPxstringrequired
Trigger (mark) price at which the order activates.
sizestring
Base-unit quantity to close (partial scale-out). Omit for the default whole-position close (sizeToMax). When set, the leg is a sized reduce-only partial capped at the live position size.
isMarketboolean
Execute as market on trigger (default true). When false, limitPrice is required.
limitPricestring
Limit price for a trigger-limit order. Required when isMarket is false.
replaceboolean
Replace an existing same-type position trigger before placing (default true).
leveragenumber
Override the inferred leverage. Defaults to the position's leverage.
isolatedboolean
Override the inferred isolated flag. Defaults to the market's margin mode.

setPositionTpsl() attaches both legs in one call (at least one of stopLossPx / takeProfitPx is required) and returns a { stopLoss?, takeProfit? } of handles. The legs derive sub-paths <path>/sl and <path>/tp. clearPositionTpsl() cancels the resting position triggers for a coin (optionally filtered to one tpsl leg) and returns the cancelled orders.

The two legs are linked as a true OCO bracket: both are stamped with one auto-generated ocoGroupId, so when either leg fills — even partially — the venue cancels the other and records cancelReason: 'sibling_filled' on it (readable via getOrder / listOrders). Pass an explicit ocoGroupId to placeOrder() to build your own sized brackets; the id is advisory metadata and is not part of the order's signature.

Pass stopLossSz / takeProfitSz (base units) to make a leg a sized partial instead of a whole-position close. Because a partial fill of one OCO leg cancels its sibling, setPositionTpsl() deliberately does not auto-link the two legs when either is sized — so a take-profit that scales out half won't cancel the stop guarding the remainder. (Pass an explicit ocoGroupId if you do want sized legs linked.)

typescript
// Bracket an open position with both legs
const { stopLoss, takeProfit } = await arca.setPositionTpsl({
path: '/op/tpsl/btc',
objectId: exchangeId,
market: 'hl:0:BTC',
stopLossPx: '54000',
takeProfitPx: '72000',
});
// Later: cancel both legs (or pass tpsl: 'sl' / 'tp' for one)
const cleared = await arca.clearPositionTpsl({
path: '/op/tpsl/btc-clear',
objectId: exchangeId,
market: 'hl:0:BTC',
});

openWithBracket(opts)

Open a position and attach its reduce-only TP/SL triggers in one atomic batch (Hyperliquid normalTpsl parity). Unlike chaining placeOrder then setPositionTpsl, the entry and its triggers are submitted as a single signed batch to one operation: the whole bracket validates and commits at the venue, or none of it does. The trigger legs arm only when the entry fills (they can't fire on mark price before the position exists), and the venue links them with a shared one-cancels-the-other group so a fill on one cancels its sibling. At least one of takeProfitPx / stopLossPx is required. Each trigger leg closes the whole position by default; set its base-unit size — takeProfitSz / stopLossSz — to make it a sized reduce-only partial instead (note size is the entry size, not the trigger size). Returns one OrderHandle per leg — { entry, takeProfit?, stopLoss? } — all backed by the single bracket operation, so each handle's .filled() / .onFill() / .cancel() targets that specific leg. Use setPositionTpsl instead to attach TP/SL to an already-open position.

Sized-partial caveat: the bracket's legs share one server-stamped OCO group, so a partial take-profit fill cancels the stop on the remainder. To “scale out half and keep a stop on the rest”, open the bracket with just the sized takeProfitSz and attach the (unsized) stop separately with setStopLoss().

typescript
const { entry, takeProfit, stopLoss } = arca.openWithBracket({
path: '/op/bracket/btc-1',
objectId: exchangeId,
market: 'hl:0:BTC',
side: 'buy',
size: '0.01',
takeProfitPx: '72000',
stopLossPx: '58000',
});
await entry; // bracket placed (entry filled or resting)
await entry.filled(); // wait for the entry to fully fill
// takeProfit / stopLoss arm automatically once the entry fills
// Scale out half at the target, keep the stop on the whole position by
// attaching it separately (the bracket's OCO would otherwise cancel a
// shared-group stop on the partial TP fill):
const { entry: e2, takeProfit: tp } = arca.openWithBracket({
path: '/op/bracket/btc-scaleout',
objectId: exchangeId,
market: 'hl:0:BTC',
side: 'buy',
size: '0.02',
takeProfitPx: '72000',
takeProfitSz: '0.01', // sized: close half at the target
});
await e2.filled();
await arca.setStopLoss({
path: '/op/bracket/btc-scaleout/sl',
objectId: exchangeId,
market: 'hl:0:BTC',
triggerPx: '58000', // unsized: protects the remaining position
}).submitted;

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

A CANCELLED order also carries an optional cancelReason (OrderCancelReason) explaining why it was cancelled — read it on-demand from getOrder / listOrders: 'user_requested', 'sibling_filled' (OCO bracket), 'position_closed', 'position_flipped', 'liquidated', or 'position_gone'.

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.

modifyOrder(opts)

Resize a resting order to a new total size. Returns an OperationHandle. Only sized orders can be resized: resting limit orders and sized TP/SL triggers. Unsized (sizeToMax: true) triggers are rejected — they always close the whole position and have no quantity to amend. Prefer order.resize(newSize) on an OrderHandle (auto-generates a per-resize path).

typescript
await arca.modifyOrder({
path: '/op/modify/btc-1-0.75',
objectId: exchangeId,
orderId: orderId,
newSize: '0.75',
});
pathstringrequired
Operation path (idempotency key). Distinct resizes need distinct paths.
objectIdstringrequired
Exchange Arca object ID.
orderIdstringrequired
ID of the order to resize.
newSizestringrequired
New total order size (base-asset units). Must exceed the already-filled quantity.

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',
market: 'hl:0:BTC',
side: 'buy',
totalSize: '0.05', // base-asset units (BTC), not USD
durationMinutes: 120,
intervalSeconds: 30,
});
console.log(twap.twap.status); // 'active'

totalSize is in base-asset units, not USD. The platform divides by the number of slices and dispatches each slice as a market order. Hyperliquid enforces a $10 minimum on every slice (sliceSize × mid); reduce-only TWAPs are exempt. Use getTwapLimits() to read the floor.

exchangeIdstringrequired
Exchange Arca object ID.
pathstringrequired
Operation path (idempotency key).
marketstringrequired
Canonical market ID (e.g. "hl:0:BTC").
side'buy' | 'sell'required
Order side.
totalSizestringrequired
Total size to execute over the duration, in base-asset units (e.g. BTC for hl:0:BTC), not USD.
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). Reduce-only TWAPs are exempt from the $10 slice floor.
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. Idempotent for terminal-state TWAPs: calling cancelTwap on a TWAP that has already completed, cancelled, or failed returns the existing record with HTTP 200 rather than a 4xx error, so a stale UI does not need to special-case the response.

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);
// twap.expectedSliceCount - planned total (durationMinutes*60 / intervalSeconds, min 1)
// twap.targetPrice - mid at TWAP creation (basis for "vs target" deltas)
// twap.failureReason - descriptive last-slice reason on terminal status === 'failed'

watchTwap(exchangeId, operationId)

Live stream of a single TWAP's state. Returns a TwapWatchStream that fetches the current TWAP via REST, then receives twap.started, twap.progress, twap.completed, twap.cancelled, and twap.failed events from the WebSocket. Each event carries the full Twap payload so the renderer never has to refetch.

typescript
const stream = await arca.watchTwap(exchangeId, operationId);
for await (const { twap } of stream) {
console.log(`${twap.sliceCount}/${twap.expectedSliceCount}`,
'status:', twap.status);
if (twap.status !== 'active') break;
}
stream.close();

listTwaps(exchangeId, activeOnly?)

List TWAPs for an exchange object.

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

getTwapLimits()

Get TWAP limits and a duration-keyed recommendation curve from the server. The response is static for the process lifetime, so the SDK caches it after the first call. Use limits for input validation and recommendedIntervalSeconds(durationMinutes) to pick a default that yields ~12-30 slices.

typescript
const { limits, recommendation } = await arca.getTwapLimits();
// limits: { minSliceNotionalUsd: 10, minIntervalSeconds: 10, maxDurationMinutes: 43200, ... }
// recommendation.buckets: [{ maxDurationMinutes: 240, recommendedIntervalSeconds: 300 }, ...]
const intervalSeconds = await arca.recommendedIntervalSeconds(60); // 5m for hour-long TWAPs

getOrderLimits()

Get venue-wide order limits. Hyperliquid enforces a $10 minimum on size × price for every non-reduce-only order. Reduce-only orders and unsized (sizeToMax: true) trigger orders are exempt so dust positions can always be closed.

typescript
const { minOrderNotionalUsd } = arca.getOrderLimits();
// 10

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:0:BTC',
limit: 50,
});
for (const fill of fills) {
console.log(fill.market, fill.side, fill.size, fill.price);
console.log(' direction:', fill.direction); // '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:0: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.previewed (preview) and fill.recorded (authoritative) WebSocket events. Each fill is a platform-levelFill with P&L, fee breakdown, trade direction, and resulting position state.

Pick the right surface: read stream.fills for an activity-feed view — it's the merged list, where each fill appears exactly once. Subscribe via stream.onUpdate(...) only when you need the raw preview→recorded transition itself (e.g. an order-confirmation animation). The update stream emits both phases as separate events, and deduping by Fill.id alone does not work because the preview's id is the venue's fill id while the recorded's id is the platform's position-ledger row — always merge by correlationId / orderId if you consume the stream directly.

typescript
const stream = await arca.watchFills(exchangeId, { market: 'hl:0:BTC' });
// stream.fills is the merged list — one row per fill, ready for an activity feed
console.log(stream.fills);
// Subscribe to onUpdate only if you need preview→recorded transitions
stream.onUpdate(({ fill, event }) => {
console.log('Fill update:', fill.market, fill.side, fill.size, fill.price);
console.log(' direction:', fill.direction, 'fee:', fill.fee, 'pnl:', fill.realizedPnl);
});
// Clean up when done
stream.close();
marketstring
Filter by market coin (e.g. hl:0: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:0:BTC');
// All coins
const settings = await arca.listLeverageSettings(exchangeId);

getActiveAssetData(objectId, coin, applicationFeeTenthsBps?, 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:0: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:0:BTC', 0, 10);
marketstring
Canonical market ID (e.g. hl:0: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 application fee (when applicationFeeTenthsBps is passed).
maintenanceMarginRatestring
Base maintenance margin rate as a decimal (e.g. "0.03" for 3%).
bidPxstring
Top-of-book best bid as a decimal string. Market sells are margin-checked at the bid, so this is the directional execution price for max-sell sizing. Equals markPx when no order book is available.
askPxstring
Top-of-book best ask as a decimal string. Market buys are margin-checked at the ask, so this is the directional execution price for max-buy sizing. Equals markPx when no order book is available.

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"
// To get a faithful cross-margin liquidation estimate, also pass accountContext
// (account equity + maintenance margin from positions in other coins +
// any existing same-coin position to merge with).
const state = await arca.getExchangeState(exchangeId);
const mmr = parseFloat(data.maintenanceMarginRate);
const otherMM = (state.positions ?? [])
.filter((p) => p.market !== 'hl:0:BTC')
.reduce((s, p) => s + mmr * parseFloat(p.size) * parseFloat(p.entryPrice), 0)
.toString();
const existingPosition = (state.positions ?? []).find((p) => p.market === 'hl:0:BTC');
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,
maintenanceMarginRate: data.maintenanceMarginRate,
accountContext: {
equity: state.marginSummary.equity,
otherMaintenanceMargin: otherMM,
existingPosition: existingPosition
? { side: existingPosition.side, size: existingPosition.size, entryPrice: existingPosition.entryPrice }
: undefined,
},
});
// 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)
// b.estimatedLiquidationPrice — cross-margin estimate for the post-fill position
// When you don't need a liquidation estimate, omit both maintenanceMarginRate
// and accountContext — the breakdown still computes the other fields.
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. Liquidation Estimate: estimatedLiquidationPrice uses the same cross-margin formula as the backend (marginAvailable = equity − maintenanceMargin, then liq = price ∓ marginAvailable / size), and applies the same merge rules to any existing same-coin position (same-side blends entry, opposite-side reduces or flips). When maintenanceMarginRate is provided, accountContext is required.

getAssetFees(objectId, applicationFeeTenthsBps?)

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 application fee.

typescript
const fees = await arca.getAssetFees(exchangeId);
for (const entry of fees) {
console.log(entry.coin, 'taker:', entry.takerFeeRate, 'maker:', entry.makerFeeRate);
}
// With application fee (40 = 4.0 bps in tenths-of-bps units)
const feesWithApplication = await arca.getAssetFees(exchangeId, 40);
marketstring
Canonical market ID (e.g. "hl:0: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() as the public asset catalog to discover supported assets, canonical IDs, category labels, icon metadata, and trading constraints. Assets listed by Hyperliquid are returned even when Arca has not curated a display name or logo yet.

Market IDs are case-sensitive. Use the asset.name value exactly as returned by getMarketMeta() for orders, leverage, active asset data, candles, order books, and price streams. Use asset.symbol, asset.venueSymbol, and asset.displayName only for display or venue links. For example, Shiba Inu is "hl:0:kSHIB", not "hl:0:KSHIB".

Canonical ID vs. display fields — the two-way rule

Every market has one canonical id (the name field, e.g. "hl:0:BTC", "hl:1:TSLA") and separate display fields (symbol, venueSymbol, displayName). They are not interchangeable — using one where the other belongs is the single most common market-data mistake:

  • Use the canonical name for every lookup and API call — orders, leverage, candles, order books, price streams, and market(). Never pass symbol, venueSymbol, or displayName to an API: a bare symbol is rejected, and a venue / display string silently matches the wrong market or none (a 200 with an empty array).
  • Never render the canonical name in your UI — show displayName ?? symbol. The canonical id (hl:1:TSLA) is an internal address, not a label for end users.
typescript
// DON'T — pass a symbol, display name, or venue id to an API
await arca.getCandles('TSLA', '1m'); // bare symbol -> 400 (rejected)
await arca.getCandles('Tesla', '1m'); // display name -> wrong asset or 400
await arca.getCandles('xyz:TSLA', '1m'); // venue / named-deployer id -> 200 with an EMPTY array
// DO — resolve the canonical name once, then pass it everywhere
const tsla = (await arca.getMarketMeta()).universe.find((a) => a.displayName === 'Tesla');
await arca.getCandles(tsla.name, '1m'); // 'hl:1:TSLA' -> full history
label.textContent = tsla.displayName ?? tsla.symbol; // UI shows 'Tesla', never tsla.name

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.categoryLabel, asset.maxLeverage, asset.szDecimals);
}

Each Market in universe contains:

namestring
Canonical market identifier to pass back to trading and market-data methods exactly as returned. IDs are case-sensitive (e.g. "hl:0:BTC", "hl:0:kSHIB", or "hl:1:TSLA" for HIP-3 assets).
symbolstring
Base asset symbol without exchange prefix (e.g. "BTC", "kSHIB"). Display-only — do not reconstruct API coin IDs from this field.
venueSymbolstring?
Venue-native market symbol for display or venue deep links (e.g. "BTC", "xyz:TSLA"). Do not pass this to Arca APIs; use name.
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.
assetTypestring?
Machine-readable category when Arca recognizes the underlying instrument, e.g. crypto, equity, commodity, index, forex, etf, hl-native, or stablecoin. Omitted for newly listed venue assets that have not been mapped yet.
categoryLabelstring?
Human-readable label derived from assetType, suitable for UI grouping.
mappedboolean?
Whether Arca recognizes the underlying instrument in its asset registry. false means the asset is live on Hyperliquid but category metadata is not available yet.
hasDisplayNameboolean?
Whether a curated displayName is available.
hasLogoboolean?
Whether a curated logoUrl or logoSources entry is available.
descriptionStatusstring?
curated when Arca has display metadata, otherwise hl_only for assets known only from the live venue listing.
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.
marginModesMarginMode[]?
The margin modes this asset supports: ['isolated'] for isolated-only markets, ['cross', 'isolated'] otherwise. Read this instead of inferring from isHip3 — margin mode is independent of HIP-3 (some HIP-3 markets, e.g. hl:1:TSLA, are cross-eligible). Orders that open or increase on isolated-only markets must include isolated: true and a positive leverage; cross orders are rejected with a 400. A reduceOnly close is accepted without leverage. closePosition() auto-fills both for you.
onlyIsolatedboolean
Deprecated — Hyperliquid-specific. Read marginModes instead. true is equivalent to marginModes being ['isolated'] (isolated-only; cross-margin not available). Removed in a future major version.
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?
Default CDN URL for the asset logo image (128px WebP). Use for asset icons in UI. Omitted when no curated logo is available. Managed via the admin panel.
logoSourcesarray?
Array of logo variants. Each source has a url, format (png|webp), and width (128|256|512). Pick the size closest to your render target to save memory.
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.

market(id)

Look up a single market by its canonical id. This is an exact-id lookup — pass the name field of a Market (e.g. "hl:0:BTC"), not a bare symbol like "BTC". To go from a human symbol to its market(s), use resolveMarkets() below. Lazily fetches and caches market metadata on first call — subsequent lookups return instantly from cache without a network request.

typescript
const btc = await arca.market("hl:0:BTC");
console.log(btc?.symbol); // "BTC"
console.log(btc?.displayName); // "Bitcoin" or undefined
console.log(btc?.logoUrl); // "https://...-128.webp" (default 128px)
console.log(btc?.logoSources); // [{ url: "...", width: 256, format: "webp" }, ...]
console.log(btc?.maxLeverage); // 50
console.log(btc?.szDecimals); // 5
// HIP-3 markets work the same way
const tsla = await arca.market("hl:1:TSLA");
console.log(tsla?.displayName); // "Tesla"
console.log(tsla?.isHip3); // true
console.log(tsla?.venueSymbol); // "xyz:TSLA"
console.log(tsla?.deployerDisplayName); // "xyz"
// Returns undefined for an unknown id — and for a bare symbol, which
// is never a canonical id (use resolveMarkets for symbols).
const nope = await arca.market("hl:0:DOESNOTEXIST"); // undefined
const alsoNope = await arca.market("BTC"); // undefined

resolveMarkets(symbol, opts?) / resolveMarketOrThrow(symbol, opts?)

Go from a human symbol ("BTC", "TSLA") to the market(s) that carry it. A single symbol can legitimately map to many markets — across exchanges and across HIP-3 dexes — so resolveMarkets returns an array. An empty array is an explicit “no market has this symbol”, never a silent guess. Matching is exact and case-sensitive on Market.symbol ("kSHIB""KSHIB"); narrow ambiguous symbols with the optional exchange / dex filters.

Use resolveMarketOrThrow for the “I expect exactly one” case — it throws a ValidationError when zero markets match (so a typo never silently no-ops) or when more than one matches (so an ambiguous symbol never silently resolves to the wrong market). If you already hold a canonical id, use market(id) instead.

typescript
// One symbol can map to many markets — resolveMarkets returns them all.
const all = await arca.resolveMarkets("BTC"); // every market whose symbol is "BTC"
// Narrow with { exchange } / { dex } when a symbol is ambiguous.
// (dex is the HIP-3 deployer DEX name; native Hyperliquid perps omit it.)
const btc = await arca.resolveMarketOrThrow("BTC", { exchange: "hl" });
await arca.placeOrder({
path: '/op/order/btc-buy-1',
objectId: exchangeId,
market: btc.name, // pass the canonical id, e.g. "hl:0:BTC"
side: 'buy',
orderType: 'MARKET',
size: '0.01',
});
// HIP-3 markets are namespaced by their deployer DEX:
const tsla = await arca.resolveMarketOrThrow("TSLA", { dex: "xyz" });
// No match is an explicit empty array, not a guess:
const none = await arca.resolveMarkets("NOTATICKER"); // []
symbolstringrequired
Display symbol to resolve (the symbol field on Market). Exact, case-sensitive.
opts.exchangestring?
Restrict to a single exchange (e.g. "hl").
opts.dexstring?
Restrict to a single HIP-3 deployer DEX by name (e.g. "xyz"). Native Hyperliquid perps omit dex, so this filter excludes them.

preloadMarketMeta() / refreshMarketMeta()

Call preloadMarketMeta() at app startup to warm the cache eagerly and avoid latency on the first market() call. Call refreshMarketMeta() to force re-fetch metadata (e.g. after a new asset is listed).

typescript
// Warm the cache at startup
await arca.preloadMarketMeta();
// Force re-fetch when metadata may have changed
await arca.refreshMarketMeta();

Other Market Endpoints

typescript
// Current mid prices (keys are canonical market IDs)
const mids: SimMidsResponse = await arca.getMarketMids();
// { mids: { 'hl:0:BTC': '98234.50', 'hl:0: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:0:BTC');
// { market: 'hl:0:BTC', bids: [...], asks: [...], time: 1234567890 }
// OHLCV candles (supports 1m, 5m, 15m, 1h, 4h, 1d)
const candles = await arca.getCandles('hl:0: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:0:BTC'] => [60100.5, 60200.1, ...] (24 hourly close prices)
// Newly listed coins may have fewer points until candle history accumulates.

watchPrices(opts?)

Subscribe to real-time mid prices. The typical pattern is to call this once at app startup and then read the current price of any asset whenever you need it — the server sends a snapshot on subscribe (and re-sends one after any reconnect), so .prices is always populated by the time the promise resolves.

Read on demand (simplest)

typescript
// Open once at startup
const prices = await arca.watchPrices();
// Read any asset's current mid, any time
const btc = prices.get('hl:0:BTC'); // decimal string, or undefined
const eth = prices.prices['hl:0:ETH']; // same thing, index form
const btcNum = prices.getNumber('hl:0:BTC'); // parsed as number (or undefined)

React to every tick

typescript
const prices = await arca.watchPrices();
const unsub = prices.onUpdate((next) => {
// `next` is a new object reference each tick — safe to store in React state
renderAssetList(next);
});

Narrow to specific assets

If you only need a few markets, pass coins to reduce bandwidth. Multiple subscribers with different coin sets share a single WebSocket subscription (the union of all requested coins).

typescript
const prices = await arca.watchPrices({
coins: ['hl:0:BTC', 'hl:0:ETH'],
});

Bound the startup wait

By default watchPrices() waits forever for the first snapshot. If you want to degrade gracefully when the backend is unreachable, pass readyTimeoutMs:

typescript
import { WatchStreamReadyTimeoutError } from '@arca-network/sdk';
try {
const prices = await arca.watchPrices({ readyTimeoutMs: 5000 });
// ...
} catch (err) {
if (err instanceof WatchStreamReadyTimeoutError) {
showOfflineBanner();
} else {
throw err;
}
}

Keys, values, and missing prices

  • Keys are canonical market IDs — e.g. "hl:0:BTC", "hl:1:TSLA". A bare symbol like "BTC" will return undefined.
  • Values are decimal strings (not floats) to preserve precision. Parse with Number(...) for quick math, or use a decimal library for correctness-critical work.
  • prices.get(coin) returns undefined for assets that haven't ticked yet or aren't tracked. The platform deliberately omits unavailable prices rather than sending "0", so undefined is a real signal — display "—" rather than a phantom zero.

Reference semantics

.prices returns a new object reference on every tick. Safe to store in React state, Zustand, or anywhere that uses reference-equality to detect changes:

typescript
prices.onUpdate((next) => {
setPrices(next); // React will re-render — reference changed
});

Connection state

Inspect stream.state to know whether prices are fresh. Values are 'loading', 'connected', or 'reconnecting'. On reconnect the server re-sends a snapshot, so prices recover automatically — but if you want to surface a "stale" indicator in the UI, subscribe to onStateChange:

typescript
prices.onStateChange((state) => {
setStale(state === 'reconnecting');
});

Cleanup

Call stream.close() when done. For the common case of "open once, live forever," you simply never close. Subscriptions are reference-counted internally, so multiple callers of watchPrices() share one WebSocket subscription.

exchangestring
Exchange identifier. Default: "sim".
coinsstring[]
Optional canonical market IDs to narrow the subscription. Omit or pass an empty array for all assets.
readyTimeoutMsnumber
Optional timeout for the initial snapshot. Rejects with WatchStreamReadyTimeoutError if exceeded. Omit to wait indefinitely.

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 '@arca-network/sdk';
const arca = new Arca({ apiKey: 'ak_...', realm: 'my-realm' });
await arca.ready();
// 1. Create a wallet to hold funds
const wallet = await arca.ensureDenominatedArca({ ref: '/wallets/main' });
// 2. Create an exchange account
const exchange = await arca.ensurePerpsExchange({ ref: '/exchanges/hl' });
// 3. Fund the wallet and transfer to the exchange (from/to are paths)
await arca.fundAccount({ arcaRef: '/wallets/main', amount: '10000' });
await arca.transfer({
path: (await arca.nonce('/op/transfer/fund-exchange')).path,
from: wallet.object.path,
to: exchange.object.path,
amount: '5000',
});
// 4. Set leverage and place an order
await arca.updateLeverage({ objectId: exchange.object.id, market: 'hl:0:BTC', leverage: 5 });
const order = await arca.placeOrder({
path: (await arca.nonce('/op/order/btc')).path,
objectId: exchange.object.id,
market: 'hl:0:BTC',
side: 'buy', size: '0.1', orderType: 'MARKET',
});
const { fills } = await order.filled();
console.log('Filled at', fills[0]?.price);
// 5. Stream exchange state for live updates
const stream = await arca.watchExchangeState(exchange.object.id);
stream.onUpdate((update) => console.log('Equity:', update.state.marginSummary.equity));
// 6. Close the position
await arca.closePosition({
path: (await arca.nonce('/op/close/btc')).path,
objectId: exchange.object.id,
market: 'hl:0:BTC',
});
// 7. Transfer remaining balance back and clean up
const balances = await arca.getBalances(exchange.object.id);
await arca.transfer({
path: (await arca.nonce('/op/transfer/withdraw-profits')).path,
from: exchange.object.path,
to: wallet.object.path,
amount: balances[0].settled,
});
stream.close();

Notes:

  • Coin IDs must be in canonical format (e.g. "hl:0:BTC", not "BTC").
  • fundAccount() is a dev helper for 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(opts?)

Subscribe to real-time mid prices. Open once at startup, then read stream.get(coin) or stream.prices[coin] any time. See the full reference for details on canonical IDs, coin filters, and ready timeouts.

typescript
// Open once at startup, subscribe to all assets.
const prices = await arca.watchPrices();
console.log(prices.get('hl:0:BTC')); // decimal string, e.g. '95432.50'
// Narrow the subscription to specific assets.
const btcOnly = await arca.watchPrices({ coins: ['hl:0:BTC'] });
// Bound the startup wait.
const fast = await arca.watchPrices({ readyTimeoutMs: 5000 });
// Push-style updates.
prices.onUpdate((next) => renderAssetList(next));
// Async iteration.
for await (const next of prices) console.log(next);
prices.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 '@arca-network/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. The per-asset maintenanceMarginRate (used to populate ActiveAssetData.maintenanceMarginRate for orderBreakdown's liquidation estimate) and the asset's marginTiers (the laddered initial-margin schedule used to size tiered assets like BTC correctly) are auto-fetched once via getActiveAssetData, or you can pass either directly to skip the lookup. The same fetch also resolves the asset's top-of-book bidPx/askPx: market buys are margin-checked at the ask and sells at the bid, so the stream sizes against that directional price (carried forward as a stable spread ratio applied to the live mid) rather than the mid alone.

maxBuyUsd/maxSellUsd are a live estimate, not a guarantee

The streamed max accounts for fees, leverage, tiered initial margin, the bid/ask spread, and existing positions and carries a small (~10 bps) safety buffer. Because mids and the spread move between preview and submission, the executable max at the venue can still drift by a few basis points. To place exactly the maximum without risking an insufficient balance rejection at the slider's upper bound, submit with useMax: true (the server sizes to the live max atomically) or pass a small sizeTolerance so the server shrinks the order to fit rather than rejecting it.

typescript
const stream = await arca.watchMaxOrderSize({
objectId: exchangeId,
market: 'hl:0:BTC',
side: 'buy',
leverage: 5,
applicationFeeTenthsBps: 40, // 4.0 bps (tenths of a bps)
szDecimals: 5,
// feeScale: omit to auto-fetch from tickers, or pass explicitly for HIP-3 assets
// maintenanceMarginRate: omit to auto-fetch (one extra REST call), or pass
// explicitly (e.g. '0.01' for BTC) to skip the lookup
});
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:0: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();
Read history from onUpdate, not a one-time read
  • Pass a canonical market id ("hl:0:BTC", "hl:1:TSLA") — a bare symbol, display name, or venue id like "xyz:TSLA" resolves to no market and the chart stays empty. Resolve a human symbol to its canonical id first with arca.resolveMarkets("BTC") (or resolveMarketOrThrow) before subscribing.
  • Subscribe via onUpdate and render from each update — do not read chart.candles once and assume it is final. On a cold market the first paint can be sparse while the SDK fills history in the background; onUpdate fires again as candles arrive.
  • History is server-side: finalized chunks come from the candle CDN and the current period plus any gaps are backfilled by the API. This works the same on staging and production — staging reads the same production CDN, so prod-listed markets (e.g. hl:0:ATOM, hl:1:TSLA) have full history there too.

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:0:BTC', 'hl:0:ETH'], ['1m', '5m']);
stream.onUpdate((event) => {
console.log(event.coin, event.interval, event.candle);
});
stream.close();

watchTrades(coins)

Subscribe to real-time market trades (the “trade tape”) for the given coins. Returns a TradeWatchStream. Each update contains a single TradeEvent with coin and trade (price, size, side, time).

typescript
const stream = await arca.watchTrades(['hl:0:BTC', 'hl:0:ETH']);
stream.onUpdate((event) => {
console.log(event.coin, event.trade.side, event.trade.px, event.trade.sz);
});
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 @arca-network/react package provides hooks that wrap each watch*() method with React lifecycle management. Cleanup is automatic on unmount.

bash
npm install @arca-network/react
typescript
import { ArcaProvider, useArcaPrices, useArcaOperations } from '@arca-network/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 @arca-network/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 '@arca-network/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, missing required field
} 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'],
});
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 on-chain custody contracts (the Arca custody kernel and coordinator 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.