| name | bluvo-sdk-ts |
| description | Use when implementing cryptocurrency exchange withdrawal flows in TypeScript without React, including optional exchange auto-swap before withdrawal. Provides a state machine that orchestrates OAuth/QR-code wallet connection, balance loading, tradable asset discovery, trade order placement and polling, quote generation, withdrawal execution with 2FA/SMS/KYC challenges (including exchange-specific multi-step 2FA for Binance, KuCoin, Coinbase, Kraken), and real-time WebSocket updates. Choose this over the React skill when building server-side integrations, non-React frontends, or custom UI frameworks. |
| license | MIT |
| compatibility | TypeScript 4.7+. Node.js 18+ for server-side use. Browser for client-side OAuth/WebSocket flows. |
| metadata | {"version":"3.1.0"} |
@bluvo/sdk-ts
Mental Model
The Bluvo SDK uses a state machine paradigm: you send events and the machine transitions through states. You do not call REST APIs directly — the BluvoFlowClient orchestrates all API calls internally and emits state changes you subscribe to.
There are two client models: BluvoClient runs server-side (requires API key, never in browser) and provides direct REST access. BluvoWebClient runs in the browser (no API key, uses OAuth popups and WebSocket). BluvoFlowClient is the high-level orchestrator that wraps both — it accepts server-side callback functions and manages the complete withdrawal flow through a nested state machine.
The flow progresses through phases: exchanges → oauth/qrcode → wallet → optional trade/convert → quote → withdraw. Each phase has loading, ready, and error states. Invalid state transitions are silently ignored (return current state unchanged), not errors.
State Diagram
idle ──→ exchanges:loading ──→ exchanges:ready ──→ oauth:waiting / qrcode:waiting
│ │
exchanges:error oauth:processing / qrcode:displaying
│
oauth:completed ←─ qrcode:scanning
│
wallet:loading
│
wallet:ready
│ │
LOAD_TRADABLE_ASSETS│ │REQUEST_QUOTE
▼ │
trade:assetsReady │
│ │
PLACE_TRADE_ORDER
│
trade:orderPlaced
│
POLL_TRADE_ORDER
│
trade:orderFilled
│
wallet:ready (refreshed)
│
quote:requesting
│
quote:ready ←─ (auto-refresh)
│
withdraw:processing
│ │ │
error2FA │ errorSMS errorKYC
error2FAMultiStep │ errorBalance
│
readyToConfirm │ retrying
│
withdraw:completed / fatal / blocked
CANCEL_FLOW → flow:cancelled (from ANY state)
Initialization
Server-side Client
import { createClient, createSandboxClient, createDevClient } from '@bluvo/sdk-ts';
const client = createClient({ orgId: '...', projectId: '...', apiKey: '...' });
const client = createSandboxClient({ orgId: '...', projectId: '...', apiKey: '...' });
const client = createDevClient({ orgId: '...', projectId: '...', apiKey: '...' });
const client = createClient({
orgId: '...', projectId: '...', apiKey: '...',
customDomain: 'api.mycompany.com'
});
Browser Flow Client
import { BluvoFlowClient } from '@bluvo/sdk-ts';
const flowClient = new BluvoFlowClient({
orgId: '...',
projectId: '...',
listExchangesFn: serverListExchanges,
fetchWithdrawableBalanceFn: serverFetchBalances,
requestQuotationFn: serverRequestQuote,
executeWithdrawalFn: serverExecuteWithdrawal,
getTradableAssetsFn: serverGetTradableAssets,
placeOrderFn: serverPlaceOrder,
getOrderFn: serverGetOrder,
getWalletByIdFn: serverGetWallet,
pingWalletByIdFn: serverPingWallet,
onWalletConnectedFn: (walletId, exchange) => { },
mkUUIDFn: () => crypto.randomUUID(),
options: {
sandbox: false,
dev: false,
maxRetryAttempts: 3,
autoRefreshQuotation: true,
customDomain: undefined,
},
cache: {
prefix: 'bluvo:',
minRemainingLifetimeSec: 15,
disabled: false,
},
});
State Reference
idle
Starting state. No flow active. Transitions to exchanges:loading or directly to oauth:waiting/qrcode:waiting.
exchanges:loading / exchanges:ready / exchanges:error
Exchange list loading phase. exchanges:ready makes the exchanges array available in context.
oauth:waiting / oauth:processing / oauth:completed / oauth:error / oauth:fatal / oauth:window_closed_by_user
OAuth popup authentication phase. oauth:error is recoverable, oauth:fatal is not (e.g., OAUTH_TOKEN_EXCHANGE_FAILED).
qrcode:waiting / qrcode:displaying / qrcode:scanning / qrcode:error / qrcode:timeout / qrcode:fatal
QR code authentication (for binance-web). Displays QR code, tracks scan status, handles expiration.
wallet:loading / wallet:ready / wallet:error
Wallet balance loading phase. wallet:ready makes walletBalances available in context.
trade:assetsLoading / trade:assetsReady / trade:assetsError
Tradable asset discovery phase. trade:assetsReady makes tradableAssets available in context, including normalized assets and spot routes such as kraken:spot:DOGE/USDC.
trade:orderPlacing / trade:orderPlaced / trade:orderPolling / trade:orderFilled / trade:orderError
Trade order phase. placeTradeOrder() places the order exactly once and stores tradeOrder with the returned orderId/txid. pollTradeOrder() then polls that order until it is filled, moves through trade:orderPolling, stores lastTradeOrderStatus, and refreshes wallet balances by default before returning to wallet:ready.
quote:requesting / quote:ready / quote:expired / quote:error
Quote phase. quote:ready makes quote available (id, amount, fee, expiresAt). Auto-refreshes by default.
withdraw:idle / withdraw:processing
Withdrawal execution phase. withdraw:processing means the withdrawal is being executed.
withdraw:error2FA / withdraw:error2FAMultiStep / withdraw:errorSMS / withdraw:errorKYC / withdraw:errorBalance
Challenge states. User must provide 2FA code, SMS code, KYC verification, or has insufficient balance.
withdraw:retrying / withdraw:readyToConfirm
retrying — withdrawal failed but can be retried. readyToConfirm — all multi-step 2FA verified, waiting for final confirmation.
withdraw:completed / withdraw:blocked / withdraw:fatal
Terminal states. completed has withdrawal.transactionId. fatal has error. blocked has error with reason.
flow:cancelled
Flow was cancelled via cancel(). Disposes withdrawal machine.
Driving Transitions
loadExchanges(status?)
Valid from: any (creates machine if needed). Result: exchanges:loading → exchanges:ready or exchanges:error.
startWithdrawalFlow({ exchange, walletId, popupOptions? })
Auto-detects QR code exchanges (binance-web). Checks if wallet exists first — if yes, delegates to resumeWithdrawalFlow. Opens OAuth popup or starts QR code flow.
resumeWithdrawalFlow({ exchange, walletId })
Skips OAuth. Transitions through OAuth states synthetically, then loads wallet balance.
silentResumeWithdrawalFlow({ walletId, exchange, preloadedBalances?, callbacks... })
Jumps directly to wallet:ready with optional preloaded balance data. Used after wallet preview.
loadTradableAssets()
Valid from: wallet:ready, trade:assetsReady, trade:assetsError. Calls getTradableAssetsFn(walletId) and stores normalized tradable assets/routes in context.tradableAssets.
placeTradeOrder({ routeId, side, type, volume, price? })
Valid from: wallet:ready, trade:assetsReady, trade:orderPlaced, trade:orderError. Calls placeOrderFn(walletId, body) once and stores the returned tradeOrder. Use the returned orderId for polling; do not retry blindly unless the previous call failed before returning an order id.
pollTradeOrder(orderId, { maxAttempts?, intervalMs?, refreshWalletOnFilled? })
Valid from: wallet:ready, trade:assetsReady, trade:orderPlaced, trade:orderError. Calls getOrderFn(walletId, orderId) until the normalized status is filled, canceled, expired, or polling times out. Defaults: maxAttempts: 30, intervalMs: 1000, refreshWalletOnFilled: true.
Auto-swap before withdrawal
Use this sequence when the wallet has a source asset but the withdrawal needs a different asset: load balances, load tradable routes, place a market order once, poll until filled, let the wallet refresh, then request a quote for the destination asset and withdraw it.
requestQuote({ asset, amount, destinationAddress, network?, tag?, includeFee? })
Valid from: wallet:ready, trade:assetsReady, trade:orderFilled, quote:ready (refresh), quote:expired. Requests a quote from the backend. Sets up auto-refresh timer if autoRefreshQuotation is true.
executeWithdrawal(quoteId)
Valid from: quote:ready. Starts withdrawal, subscribes to WebSocket for progress and completion.
submit2FA(code)
Valid from: withdraw:error2FA. Re-executes withdrawal with TOTP code.
submit2FAMultiStep(stepType, code)
Valid from: withdraw:error2FAMultiStep. Submits code for one step ('GOOGLE', 'EMAIL', or 'SMS'). For incremental-verification exchanges (binance-web, bybit-web) it re-invokes the withdrawal immediately with all collected codes. For batch-validation exchanges (KuCoin) with relation: 'AND', it only collects the code (via the COLLECT_2FA_MULTI_STEP action — no API call, state stays withdraw:error2FAMultiStep, returns { success: true, result: { collected: true, awaitingSteps } }) until every required code-based step has a code; the final code triggers exactly one withdrawal execution with bizNo + all collected codes. See Gotcha 13.
pollFaceVerification()
Valid from: withdraw:error2FAMultiStep. Transitions to processing to check FACE step completion.
pollRoamingFidoVerification()
Valid from: withdraw:error2FAMultiStep. Polls backend to check ROAMING_FIDO step completion (no state transition during poll — MFA UI stays visible).
confirmWithdrawal()
Valid from: withdraw:readyToConfirm. Sends final confirmation after all 2FA steps verified.
retryWithdrawal()
Valid from: withdraw:retrying. Re-executes withdrawal with new idempotency key.
refreshQRCode()
Valid from: qrcode:error or qrcode:timeout. Generates a new QR code.
subscribe(listener), getState(), cancel(), dispose()
Standard machine operations. cancel() transitions to flow:cancelled. dispose() cleans up timers and subscriptions.
Error Handling
Error Codes
The ERROR_CODES constant contains 40+ error codes grouped by category:
- Generic:
GENERIC_NOT_FOUND, GENERIC_UNAUTHORIZED, GENERIC_INTERNAL_SERVER_ERROR, etc.
- Wallet:
WALLET_NOT_FOUND, WALLET_INVALID_CREDENTIALS
- Trading: trading APIs surface validation/provider errors via backend error codes and may return
APIKEY_INSUFFICIENT_PERMISSIONS when the project key lacks trading scopes.
- Quote:
QUOTE_NOT_FOUND, QUOTE_EXPIRED
- Withdrawal Balance:
WITHDRAWAL_INSUFFICIENT_BALANCE, WITHDRAWAL_INSUFFICIENT_BALANCE_FOR_FEE
- Withdrawal Address:
WITHDRAWAL_INVALID_ADDRESS, WITHDRAWAL_NETWORK_NOT_SUPPORTED
- Withdrawal Amount:
WITHDRAWAL_AMOUNT_BELOW_MINIMUM, WITHDRAWAL_AMOUNT_ABOVE_MAXIMUM
- Withdrawal 2FA:
WITHDRAWAL_2FA_REQUIRED_TOTP, WITHDRAWAL_2FA_REQUIRED_SMS, WITHDRAWAL_2FA_REQUIRED_MULTI_STEPS, WITHDRAWAL_2FA_INVALID, etc. (supports GOOGLE, EMAIL, FACE, SMS, ROAMING_FIDO steps)
- OAuth:
OAUTH_AUTHORIZATION_FAILED, OAUTH_TOKEN_EXCHANGE_FAILED (FATAL)
- Special:
WITHDRAWAL_DRY_RUN_COMPLETE — NOT an error, it's a success signal
Error Extraction
import { extractErrorCode, extractErrorTypeInfo, extractErrorResult, ERROR_CODES } from '@bluvo/sdk-ts';
const code = extractErrorCode(error);
const info = extractErrorTypeInfo(error);
const result = extractErrorResult(error);
if (code === ERROR_CODES.WITHDRAWAL_2FA_REQUIRED_TOTP) {
}
Error States
oauth:error — Recoverable OAuth error (user can retry)
oauth:fatal — Non-recoverable OAuth error (wallet connection broken)
withdraw:error2FA — 2FA required, user must submit code
withdraw:error2FAMultiStep — Multi-step 2FA required
withdraw:fatal — Non-recoverable withdrawal error
withdraw:errorBalance — Insufficient balance
Gotchas
-
toPlain() needed for Next.js server actions. The SDK returns class instances that Next.js cannot serialize. Wrap every server action return with JSON.parse(JSON.stringify(result)).
-
QR code flow is auto-detected for binance-web. When you call startWithdrawalFlow({ exchange: 'binance-web', ... }), it automatically routes to the QR code flow instead of opening an OAuth popup. The constant QR_CODE_EXCHANGES = ['binance-web'] controls this.
-
autoRefreshQuotation defaults to true. Quotes auto-refresh when they expire. If you want manual control (showing an "expired" UI), set autoRefreshQuotation: false in options.
-
Invalid state transitions are no-ops. Sending an action in the wrong state returns the current state unchanged — no error is thrown. Check getState().type if you need to verify the transition happened.
-
multiStep2FA.mfa.verified is the PRIMARY source of truth for step verification status, not step.status. The mfa.verified object is updated by the backend and reflects the actual verification state. Supported keys: GOOGLE, EMAIL, FACE, SMS, ROAMING_FIDO.
-
WITHDRAWAL_DRY_RUN_COMPLETE is NOT a real error. It's a success signal indicating all multi-step 2FA steps are verified and the withdrawal is ready to be confirmed. The SDK handles it by transitioning to withdraw:readyToConfirm.
-
OAuth window close detection uses 500ms polling interval. There's a slight delay between the user closing the popup and the oauth:window_closed_by_user state transition.
-
Cache has 15-second safety threshold. QR code cache entries with less than 15 seconds remaining are treated as expired and ignored. Configurable via cache.minRemainingLifetimeSec.
-
startWithdrawalFlow checks if wallet exists first. If getWalletByIdFn returns a wallet, it automatically calls resumeWithdrawalFlow instead of opening OAuth.
-
Trading callbacks are optional but required for auto-swap. If getTradableAssetsFn, placeOrderFn, or getOrderFn are missing, the flow moves to a trade error state and returns "Trading API callbacks are not configured".
-
placeTradeOrder() is a one-shot action. It calls the backend Place Order API once and returns the exchange order id/txid. Persist or hold that id in UI state before polling so refreshes do not accidentally place a second order.
-
Filled trade polling refreshes balances by default. pollTradeOrder() calls loadWallet() after a filled order unless refreshWalletOnFilled: false is passed. Because of that, the visible final state is normally wallet:ready, with lastTradeOrderStatus.status === "filled" still available in context.
-
KuCoin validates all 2FA factors in ONE call (batch validation). KuCoin's broker withdrawal API has no incremental per-factor verification — re-executing with a partial factor set (e.g. only the Google code when EMAIL + GOOGLE are required with relation: 'AND') makes KuCoin answer 200000 with no ids and the challenge dies server-side (historically a fatal KuCoin withdrawal failed: unexpected response shape). The SDK therefore treats exchanges in BATCH_2FA_VALIDATION_EXCHANGES (currently ['kucoin']) specially when relation === 'AND': each submit2FAMultiStep() call stores the code in collectedCodes (GOOGLE → twofa, EMAIL → emailCode, SMS → smsCode) and marks that step status: 'success' locally so the UI advances — the withdrawal endpoint is NOT called and codes are NOT validated yet. Only the final missing code triggers a single execution with bizNo + all collected codes. Steps already mfa.verified === true count as satisfied; FACE/ROAMING_FIDO are excluded (poll-verified). Consequences: (a) for KuCoin a locally-success step does not mean the backend verified that code — an invalid code only surfaces after the final batch call; (b) with relation: 'OR' KuCoin behaves like other exchanges (one code → immediate re-execution).
References
- Read
references/api-client.md if you need to understand the underlying REST calls, error codes, or authentication configuration.
- Read
references/types.md if you need the complete TypeScript type definitions for machine context, events, or state values.
- Read
references/state-transitions.md if you need detailed guard conditions, async behavior, or sequence diagrams for specific flows.
- Read
../react/skill/references/qrcode-binance-web.md for comprehensive QR code authentication flow implementation patterns including component rendering for binance-web.
- Read
../react/skill/references/multistep-2fa.md for comprehensive multi-step 2FA implementation patterns including FACE polling, ROAMING_FIDO polling, dryRun pattern, and step verification.