SDK: Installation & Setup
The @arca/sdk package provides a high-level, typed, idempotent interface for building on Arca. It requires Node.js 18+ and is written in TypeScript (ES2022).
Installation
npm install @arca/sdkPackage Exports
The package exports one main class and all supporting types:
import { Arca } from '@arca/sdk';
// Types are also exported for strong typingimport type { ArcaObject, ArcaBalance, Operation, TransferOptions, PlaceOrderOptions, RealmEvent, // ... 70+ exported types} from '@arca/sdk';Initialization
The SDK supports two authentication modes. In both cases, call arca.ready() to resolve the realm before making API calls (it is also called automatically on the first API call).
// API Key — for backend services, scripts, CIconst arca = new Arca({ apiKey: 'arca_78ae7276_...', realm: 'development', // slug or UUID baseUrl: 'http://localhost:8080', // optional});await arca.ready();
// Scoped Token — for end-user frontendsconst arca = Arca.fromToken(scopedJwt);// realm is extracted from the token claims automaticallyawait arca.ready();SDK: Authentication
The SDK mirrors the platform's dual-auth model. Choose the right mode for your context:
API Key (Backend)
API keys are team-scoped with full access across all realms. Use this for server-side code, scripts, and CI pipelines.
import { Arca } from '@arca/sdk';
const arca = new Arca({ apiKey: 'arca_78ae7276_...', realm: 'development',});apiKeystringrequiredrealmstringrequireddevelopment) or UUID.baseUrlstringhttps://api.arca.dev.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.
// The builder's backend mints this token and passes it to the frontendconst arca = Arca.fromToken(scopedJwt);
// The realm is extracted from the token claims automaticallyawait arca.ready();If the token does not embed a realmId claim, provide it explicitly:
const arca = Arca.fromToken(scopedJwt, { realm: 'development' });tokenstringrequiredPOST /auth/token.realmstringrealmId claim.baseUrlstringhttps://api.arca.dev.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.
SDK: Arca Objects
createDenominatedArca(opts)
Create a denominated Arca object at the given path. Idempotent — if one already exists with matching type and denomination, it is returned.
const { object, operation } = await arca.createDenominatedArca({ ref: '/wallets/main', denomination: 'USD', operationPath: '/op/create/wallets/main:1', // optional idempotency key});refstringrequireddenominationstringrequiredUSD, BTC).metadatastringoperationPathstringcreateArca(opts)
Create an Arca object of any type at the given path.
const { object } = await arca.createArca({ ref: '/deposits/d1', type: 'deposit', denomination: 'USD',});refstringrequiredtypestringrequireddenominated, exchange, deposit, withdrawal, escrow.denominationstringdenominated type.metadatastringoperationPathstringgetObject(path)
Get the active Arca object at a given path.
const obj: ArcaObject = await arca.getObject('/wallets/main');console.log(obj.id, obj.status, obj.denomination);getObjectDetail(objectId)
Get full object detail including operations, events, state deltas, and balances.
const detail = await arca.getObjectDetail(obj.id);console.log(detail.operations.length); // all operations on this objectconsole.log(detail.balances); // current balancesconsole.log(detail.reservedBalances); // in-flight reserved amountslistObjects(opts?)
List Arca objects in the realm, optionally filtered by path prefix.
const { objects, total } = await arca.listObjects({ prefix: '/wallets', includeDeleted: false, // default});getBalances(objectId)
Get current balances for an Arca object.
const balances: ArcaBalance[] = await arca.getBalances(obj.id);// [{ id: '...', arcaId: '...', denomination: 'USD', amount: '1000.00' }]ensureDeleted(opts)
Delete an Arca object by path. 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.
// Simple deletion (no balance)const { object, operation } = await arca.ensureDeleted({ ref: '/wallets/old',});
// Deletion with balance sweepconst { object, operation } = await arca.ensureDeleted({ ref: '/wallets/old', sweepTo: '/wallets/main',});
// Exchange deletion with position liquidationconst { object, operation } = await arca.ensureDeleted({ ref: '/exchanges/hl1', sweepTo: '/wallets/main', liquidatePositions: true,});refstringrequiredsweepTostringSDK: Transfers & Deposits
transfer(opts)
Execute a transfer between two Arca objects. 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.
const { operation } = await arca.transfer({ path: '/op/transfer/alice-to-bob-1', from: '/wallets/alice', to: '/wallets/bob', amount: '250.00',});
console.log(operation.state); // 'completed' for denominated→denominatedconsole.log(operation.outcome); // JSON with new balancespathstringrequiredfromstringrequiredtostringrequiredamountstringrequireddeposit(opts)
Initiate a deposit to a denominated Arca object. In demo realms, deposits are simulated with a configurable delay and success/failure.
const { operation } = await arca.deposit({ arcaRef: '/wallets/main', amount: '1000.00', durationSeconds: 5, // default: 5 willSucceed: true, // default: true});
// operation.state === 'pending' (settlement happens asynchronously)// Listen for deposit.completed event via events.subscribe()arcaRefstringrequiredamountstringrequireddurationSecondsnumberwillSucceedbooleanSDK: Operations & Events
getOperation(operationId)
Get operation detail by ID, including correlated events and state deltas.
const detail = await arca.getOperation(operationId);console.log(detail.operation.type); // 'transfer', 'deposit', etc.console.log(detail.operation.state); // 'pending', 'completed', 'failed'console.log(detail.events); // correlated eventsconsole.log(detail.deltas); // state deltas (before/after values)listOperations(opts?)
List operations in the realm, optionally filtered by type.
const { operations, total } = await arca.listOperations({ type: 'transfer', // optional filter});typeOperationTypetransfer, create, delete, deposit, withdrawal, swap, order, cancel.nonce(prefix, separator?)
Reserve the next unique nonce for a path prefix. Returns a path suitable for use as an idempotency key on operations or object creation.
// Default separator (hyphen)const { nonce, path } = await arca.nonce('/op/transfer/fund');// { nonce: 1, path: '/op/transfer/fund-1' }
// Colon separator for create-operation noncesconst { path: opPath } = await arca.nonce('/op/create/wallets/main', ':');// '/op/create/wallets/main:1'
// Use the reserved path as the idempotency keyawait arca.transfer({ path, from: '/wallets/alice', to: '/wallets/bob', amount: '100' });prefixstringrequired/op/transfer/fund).separatorstring/ if prefix ends with /, otherwise -. Use ":" for operation nonces.Always call nonce() before the operation and store the resulting path. On retry, reuse the stored path — that is what makes the operation idempotent. Never call nonce() inline inside an operation call or inside a retry loop; each invocation produces a new unique path, turning every retry into a distinct operation.
// GOOD — reserve once, store, reuse on retryconst { path } = await arca.nonce('/op/transfer/fund');// persist `path` in your application state if neededawait arca.transfer({ path, from: '/wallets/a', to: '/wallets/b', amount: '100' });
// BAD — new nonce on every call defeats idempotencyawait arca.transfer({ path: (await arca.nonce('/op/transfer/fund')).path, // different path every time! from: '/wallets/a', to: '/wallets/b', amount: '100',});summary()
Get aggregate counts for the realm.
const summary = await arca.summary();console.log(summary.objectCount); // 12console.log(summary.operationCount); // 47console.log(summary.eventCount); // 93SDK: 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.
createPerpsExchange(opts)
Create a perps exchange Arca object. Denomination is automatically set to USD.
const { object } = await arca.createPerpsExchange({ ref: '/exchanges/hl1', exchangeType: 'hyperliquid', // default});refstringrequiredexchangeTypestringhyperliquid.operationPathstringgetExchangeState(objectId)
Get exchange account state including equity, margin, positions, and open orders.
const state: ExchangeState = await arca.getExchangeState(objectId);console.log(state.account.usdBalance);console.log(state.marginSummary.accountValue);console.log(state.positions); // SimPosition[]console.log(state.openOrders); // SimOrder[]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.
await arca.updateLeverage({ objectId: exchangeId, coin: 'BTC', leverage: 10,});objectIdstringrequiredcoinstringrequiredleveragenumberrequiredplaceOrder(opts)
Place a market or limit order. The path is the idempotency key. Orders use the leverage currently set for the coin.
const { operation } = await arca.placeOrder({ path: '/op/order/btc-buy-1', objectId: exchangeId, coin: 'BTC', side: 'BUY', orderType: 'MARKET', size: '0.01',});pathstringrequiredobjectIdstringrequiredcoinstringrequiredBTC, ETH).side'BUY' | 'SELL'requiredorderType'MARKET' | 'LIMIT'requiredsizestringrequiredpricestringLIMIT orders.reduceOnlybooleantimeInForcestringGTC (default), IOC, ALO.listOrders(objectId, status?)
List orders for an exchange Arca object, optionally filtered by status.
const orders: SimOrder[] = await arca.listOrders(exchangeId);const openOrders = await arca.listOrders(exchangeId, 'OPEN');getOrder(objectId, orderId)
Get a specific order with its fill history.
const { order, fills }: SimOrderWithFills = await arca.getOrder(exchangeId, orderId);console.log(order.status); // 'FILLED'console.log(fills.length); // number of fillscancelOrder(opts)
Cancel an open order. The path is the idempotency key.
const { operation } = await arca.cancelOrder({ path: '/op/cancel/btc-1', objectId: exchangeId, orderId: orderId,});pathstringrequiredobjectIdstringrequiredorderIdstringrequiredlistPositions(objectId)
List current positions for an exchange Arca object.
const positions: SimPosition[] = await arca.listPositions(exchangeId);for (const pos of positions) { console.log(pos.coin, pos.side, pos.size, pos.unrealizedPnl);}Market Data
Global market data endpoints — not scoped to a specific exchange object.
// Market metadata (supported assets, max leverage, etc.)const meta: SimMetaResponse = await arca.getMarketMeta();// { universe: [{ name: 'BTC', maxLeverage: 50, ... }, ...] }
// Current mid pricesconst mids: SimMidsResponse = await arca.getMarketMids();// { mids: { 'BTC': '98234.50', 'ETH': '2847.30', ... } }
// L2 order bookconst book: SimBookResponse = await arca.getOrderBook('BTC');// { coin: 'BTC', bids: [...], asks: [...], time: 1234567890 }SDK: Real-time Streaming
Subscribe to real-time realm events via Server-Sent Events (SSE). The SDK manages the EventSource connection and provides client-side prefix filtering.
events.subscribe(opts, callback)
// Subscribe to all eventsconst sub = arca.events.subscribe((event: RealmEvent) => { console.log(event.type, event.entityId, event.entityPath);});
// Subscribe with prefix filter (client-side)const sub = arca.events.subscribe( { prefix: '/op/' }, (event) => { console.log('Operation event:', event.type); },);
// Close the subscription when donesub.close();Event Types
The following event types are emitted by the stream:
| Event Type | Description |
|---|---|
object.created | New Arca object created (carries creation delta) |
object.updated | Object metadata or properties changed |
object.deleted | Object finalized as deleted (carries status_change + deletion deltas) |
object.status_changed | Object status transitioned (e.g., active → deleting, or deleting → active on revert) |
operation.created | New operation created |
operation.updated | Operation state changed |
event.created | New event recorded |
balance.updated | Balance changed on an object |
deposit.initiated | Deposit initiated, inbound hold created (carries hold_change delta) |
deposit.completed | Deposit settled successfully (carries balance_change + hold_change deltas) |
deposit.failed | Deposit failed (carries hold_change delta for inbound hold cancellation) |
transfer.initiated | Async transfer source debit recorded (carries balance_change + hold_change deltas) |
transfer.outbound_released | Outbound hold released during settlement (carries hold_change delta) |
transfer.completed | Transfer settled (carries balance_change + hold_change deltas) |
transfer.failed | Transfer failed, funds returned to source (carries reversal balance_change + hold_change deltas) |
order.filled | Exchange order filled (carries settlement_change + position_change deltas) |
order.cancelled | Exchange order cancelled (informational, no deltas) |
sweep.completed | Balance sweep during deletion (carries balance_change deltas for source + target) |
withdrawal.completed | Exchange funds withdrawn to Arca balance (carries balance_change delta) |
liquidation.completed | Exchange positions liquidated (carries settlement_change + position_change deltas) |
RealmEvent Shape
interface RealmEvent { realmId: string; type: string; // e.g. 'deposit.completed' entityId: string; // ID of the affected entity entityPath: string; // path of the affected entity}Subscription Options
prefixstringentityPath starts with this prefix are delivered to the callback.SDK: Error Handling
The SDK maps API error responses to typed error classes. All errors extend ArcaError, which provides a machine-readable code and a human-readable message.
Error Classes
| Class | Code | HTTP Status | When |
|---|---|---|---|
ValidationError | VALIDATION_ERROR | 400 | Invalid input |
UnauthorizedError | UNAUTHORIZED | 401 | Missing or invalid auth |
NotFoundError | NOT_FOUND | 404 | Resource not found |
ConflictError | CONFLICT | 409 | Duplicate or invalid state |
InternalError | INTERNAL_ERROR | 500 | Unexpected server error |
Usage Pattern
import { Arca, ValidationError, NotFoundError, ConflictError } from '@arca/sdk';
try { await arca.transfer({ path: '/op/transfer/fund-1', from: '/wallets/alice', to: '/wallets/bob', amount: '250.00', });} catch (err) { if (err instanceof ValidationError) { console.error('Invalid input:', err.message); // e.g., insufficient balance, denomination mismatch } else if (err instanceof NotFoundError) { console.error('Object not found:', err.message); } else if (err instanceof ConflictError) { console.error('Conflict:', err.message); // e.g., duplicate resource, object being deleted } else { throw err; // unexpected }}Base Error Class
class ArcaError extends Error { public readonly code: string; // machine-readable code public readonly message: string; // human-readable description}All error classes (ValidationError, UnauthorizedError, etc.) extend ArcaError and set the appropriate code. Unknown error codes are returned as a base ArcaError instance.
SDK: Org & Team Management
The ArcaAdmin class provides methods for managing your organization, team members, and invitations.
Get Organization
const admin = new ArcaAdmin({ token });const org = await admin.getOrg();console.log(org.name, org.slug);List Members
const { members, total } = await admin.listMembers();for (const m of members) { console.log(m.email, m.role, m.realmTypeScope);}Invite a Member
const invitation = await admin.inviteMember({ email: 'alice@example.com', role: 'developer', realmTypeScope: ['development', 'staging'],});console.log(invitation.inviteLink);Update Member Role
await admin.updateMember('member-id', { role: 'admin', realmTypeScope: null, // null = all realm types});Remove Member
await admin.removeMember('member-id');Transfer Ownership
await admin.transferOwnership('new-owner-user-id');