CRISP partners

Shared account state

Fast independent cash and share reads with a shared browser-safe SDK store.

SDK 0.13.0 adds @crisp-trade/partner-sdk/account-state. One store per app provider owns request sharing, polling, expiry, retries and account-switch safety. Keep CRISP credentials on your server. No new service is required.

Names and boundaries

ResourceMeaning
cashBalanceDisplay cash held by the managed wallet, not portfolio value
availableCashAll-in spending budget after existing reservations and fees
availableShares(tokenId)Shares currently available to sell for one token

Use decimal strings throughout financial calculations. null means unknown, never zero. Available cash is not a withdrawal amount or a maximum BUY notional: include the new order's fees within that budget. The backend still validates every submission. The store does not reserve money or replay orders.

Your backend

Expose authenticated same-origin routes. Resolve the CRISP external user ID from the verified session, never from an unchecked browser parameter.

// Separate route handlers: neither waits for the other.
const cash = await crisp.getFunding(user.externalUserId, { details: 'summary' });
// Return cash.data.balance?.formatted ?? null from /api/account/cash.

const available = await crisp.getTradingAvailability(user.externalUserId);
// Return available.data.availableToTrade?.formatted ?? null
// from /api/account/available-cash. Requires wallet:read.

getFunding(..., { details: 'trading' }) remains compatible but combines reads. Prefer the independent paths for progressive UI. Deposit/withdrawal dialogs can load details: 'full' on demand. Never use display cash as an availability fallback. See balances for the HTTP field contracts.

Browser store

The following BFF example returns { amount: string | null } for amounts and { positions: [] } for positions. Adapt your BFF's envelope once here, not in every component. Share token IDs are decimal strings, not condition IDs.

import { createAccountStore, decimalAmount } from '@crisp-trade/partner-sdk/account-state';

async function read(path: string, signal: AbortSignal) {
  const response = await fetch(path, { signal, credentials: 'same-origin', cache: 'no-store' });
  if (!response.ok) throw new Error('Account unavailable');
  return response.json();
}

const store = createAccountStore({
  loaders: {
    cashBalance: async ({ signal }) => decimalAmount((await read('/api/account/cash', signal)).amount),
    availableCash: async ({ signal }) => decimalAmount((await read('/api/account/available-cash', signal)).amount),
    positions: async ({ signal }) => (await read('/api/account/positions', signal)).positions,
  },
  loadAvailableShares: async (tokenId, { signal }) =>
    decimalAmount((await read(`/api/account/shares?tokenId=${encodeURIComponent(tokenId)}`, signal)).amount),
});

store.setAccount(sessionKey); // Opaque identity that changes when the session/account changes.
const available = store.field('availableCash');
const stop = available.subscribe(() => render(available.getSnapshot()));
// The snapshot updates immediately, independently of positions/cash/history.
// On screen unmount: stop(). On logout/provider unmount: store.clearAccount().

Construction does no I/O. Mount a store in a client provider, not a server module singleton. cashBalance and availableCash preload by default. Override preload to add positions when a persistent header displays portfolio value. Other fields load when subscribed, or through explicit refresh. Two subscribers share one request and a fresh snapshot; unsubscribing stops their fallback polls.

React adapter

The core has no React dependency. Create the store once with useState, expose it through your context, and use stable handles:

const field = store.field('availableCash');
const cash = useSyncExternalStore(field.subscribe, field.getSnapshot, field.getServerSnapshot);
// cash.data: string | null; cash.refreshing does not erase a fresh value.
// cash.status: idle | loading | ready | unavailable
// cash.freshness: unknown | fresh | stale

Use the same pattern with a memoized store.availableShares(tokenId) handle. Never call setAccount or start network reads during render. In a lifecycle effect set the current session key and clear it on cleanup; also clear synchronously on logout before another account is shown.

Realtime and order feedback

Forward the app's existing realtime stream:

store.onEvent(event.type);             // Notification callback
store.onEvent('reconciliation');       // Reconnect/resync callback
store.afterOrder('accepted');          // Accepted, including a resting limit order
store.afterOrder('filled');            // Confirmed execution
store.afterOrder('unknown');           // Ambiguous response; reconcile, do not resubmit
store.afterOrder('rejected');          // No invalidation for a definite rejection

Pass only results for the current session. Preserve your command's session guard when awaiting a request across logout. The store never calls order APIs itself. Events immediately invalidate affected availability and coalesce reads over 50 ms. Post-order/fill reconciliation uses a bounded 1/3/10/30/60/120-second schedule shared by overlapping fills, plus ordinary polling. Only preloaded and subscribed fields are refreshed. Existing createAccountSync remains compatible, but do not run it alongside this store for the same reads.

Call pause() while the page is hidden; resume() when visible or back online. Default polling is 30 seconds, share polling/expiry 15 seconds, cash availability expiry 60 seconds, and read deadline 10 seconds. Three fast retries use bounded backoff before returning to ordinary polling. Deadlines still settle when a loader ignores abort. Account changes abort and fence all old responses.

Display resources retain stale data on failure; availability is cleared on failure, expiry or invalidation. Show loading/unavailable explicitly. Polling, expiry, and timeouts are not latency guarantees: measure real transport/RPC latency independently. Optional onTiming reports resource, duration and success only, without account IDs, balances, tokens or credentials.

CRISP Partner Documentation