# API Keys Source: https://developer.satsterminal.com/api-keys How to obtain and use your SatsTerminal API key. # Rate Limits & API Keys Our API is free to integrate but rate-limited. An API key is required for all requests (except when using the Embed). * Default limits: **5 requests per second** across all endpoints. * Exceeding the limit triggers a **5-minute ban**. * Need more throughput? **Contact us** and we can raise limits for approved partners. Apply for an API key: [Sats Terminal Partnership Form](https://docs.google.com/forms/d/e/1FAIpQLSeTH9w1hVZQRLpu0gPjTM67gauuU2G95sy6vWpI9EJkb6Iz4g/viewform?usp=send_form) ## Getting an API key 1. Log into the SatsTerminal dashboard. 2. Create or view your API keys in sidebar menu. 3. Keep keys secret; do not commit them to source control. ## Using the key Pass the key when initializing any client: ```ts theme={null} import { SatsTerminal } from "satsterminal-sdk"; const swaps = new SatsTerminal({ apiKey: process.env.API_KEY! }); ``` or with the suite: ```ts theme={null} import { createClient } from "satsterminal-sdk"; const { swaps, borrow } = createClient({ apiKey: process.env.API_KEY!, borrow: { /* ... */ } }); ``` > Using the Embed? You **do not** need an API key—the widget handles auth for you. ## Best practices * Store keys in environment variables or a secrets manager. * Rotate keys periodically. * Use different keys per environment (dev/staging/prod). # Borrow sdk Source: https://developer.satsterminal.com/borrow/api-reference/borrow-sdk # BorrowSDK The main class for interacting with SatsTerminal Borrow. ## Constructor ```typescript theme={null} new BorrowSDK(config: BorrowSDKConfig) ``` Creates a new SDK instance. ### Parameters | Parameter | Type | Description | | --------- | ----------------- | ----------------- | | `config` | `BorrowSDKConfig` | SDK configuration | ### Example ```typescript theme={null} import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; const sdk = new BorrowSDK({ apiKey: 'your-api-key', wallet: { address: 'bc1q...', signMessage: async (msg) => wallet.signMessage(msg) } }); ``` *** ## Utility Exports The SDK also exports utility functions for unit conversion and response handling: ```typescript theme={null} import { BorrowSDK, ChainType, Units, // Type-safe BTC/satoshi conversions ResponseNormalizer, // API response normalization type Satoshis, // Branded type for satoshis type BTC, // Branded type for BTC } from '@satsterminal-sdk/borrow'; // Convert units const btc = Units.satsToBtc(10000000); // "0.10000000" const sats = Units.btcToSats("0.1"); // 10000000 // Auto-detect and normalize units const normalized = Units.normalizeToBtc(value); // Handle various API response formats const quotes = ResponseNormalizer.normalizeQuotes(apiResponse); ``` See [Types Reference](/borrow/api-reference/types#utility-types) for full documentation. *** ## Properties ### `userStatus` Current user status. ```typescript theme={null} readonly userStatus: UserStatus ``` ### `platformWalletAddress` The platform smart wallet address (index 0). ```typescript theme={null} readonly platformWalletAddress: string | null ``` *** ## Methods ### setup() Optional account preload for dashboards and manual account flows. Without options, the SDK uses the active chain if one is already selected, otherwise Base as the preload chain. Pass `options.chain` only when you intentionally want to preload a different chain. `executeBorrow()` derives the chain from the selected quote and can prepare the required borrow wallet/session state automatically. ```typescript theme={null} async setup(options?: { chain?: ChainType }): Promise<{ platformWallet: { address: string; signature: string }; userStatus: UserStatus; activeSession: ActiveSession; transactions: UserTransaction[]; }> ``` #### Returns | Property | Type | Description | | ---------------- | ------------------- | ------------------------------------- | | `platformWallet` | `object` | Platform wallet address and signature | | `userStatus` | `UserStatus` | Current user status | | `activeSession` | `ActiveSession` | Active session details | | `transactions` | `UserTransaction[]` | Recent transactions | #### Example ```typescript theme={null} const { platformWallet, userStatus, activeSession } = await sdk.setup(); console.log('Smart Account:', platformWallet.address); ``` Use this when you want to load account status, session details, and transaction history before the user starts a borrow. Borrow-only flows can skip it. *** ### startNewLoan() Advanced method to create a new isolated loan wallet/session manually. Most borrow flows should use `getQuotes()` and `executeBorrow()`. ```typescript theme={null} async startNewLoan(options?: { chain?: ChainType }): Promise<{ userStatus: UserStatus; activeSession: ActiveSession; }> ``` #### Example ```typescript theme={null} const { userStatus, activeSession } = await sdk.startNewLoan(); // Now ready to execute borrow ``` *** ### getQuotes() Get available loan quotes. ```typescript theme={null} async getQuotes(params: QuoteRequest): Promise ``` #### Parameters | Parameter | Type | Required | Description | | ------------------ | -------- | -------- | ------------------ | | `collateralAmount` | `string` | Yes | Collateral in BTC | | `loanAmount` | `string` | Yes | Loan amount in USD | #### Example ```typescript theme={null} const quotes = await sdk.getQuotes({ collateralAmount: '0.1', loanAmount: '5000' }); ``` *** ### getWebhookConfig() Get the webhook configuration for the SDK API key. ```typescript theme={null} async getWebhookConfig(): Promise ``` *** ### updateWebhookConfig() Create or update the webhook configuration for the SDK API key. ```typescript theme={null} async updateWebhookConfig(input: SdkWebhookConfigUpdateRequest): Promise ``` #### Example ```typescript theme={null} await sdk.updateWebhookConfig({ enabled: true, url: 'https://api.example.com/webhooks/satsterminal', events: [SdkWebhookEventType.BTC_DEPOSIT_CONFIRMED], }); ``` *** ### rotateWebhookSecret() Rotate the HMAC signing secret used for webhook delivery. The new secret is returned once. ```typescript theme={null} async rotateWebhookSecret(): Promise ``` #### Example ```typescript theme={null} const { secret } = await sdk.rotateWebhookSecret(); ``` *** ### executeBorrow() Execute a borrow with a selected quote. This method prepares the platform wallet, loan wallet, and session automatically when needed. ```typescript theme={null} async executeBorrow(quote: Quote, options?: { destinationAddress?: string; }): Promise ``` #### Returns `string` - Workflow ID If `setup()` was called immediately before, `executeBorrow()` reuses that prepared unused loan wallet. Otherwise it creates a fresh isolated loan wallet/session for the borrow. #### Example ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote, { destinationAddress: '0x...' }); await sdk.trackWorkflow(workflowId, { onStatusUpdate: (status) => console.log(status.label), onDepositReady: (info) => console.log(`Deposit ${info.amountBTC} BTC to ${info.address}`), onComplete: () => console.log('Borrow complete') }, 'borrow'); ``` *** ### repay() Repay a loan. The SDK prepares the platform wallet address/signature automatically when needed; callers do not need to call `setup()` first. The loan chain is inferred from the original borrow transaction; pass `options.chain` only as an explicit override. ```typescript theme={null} async repay( originalBorrowId: string, repayAmount: string, options?: { chain?: ChainType; useCollateral?: boolean; collateralToWithdraw?: string; userBtcWithdrawAddress?: string; trackWorkflow?: boolean; callbacks?: WorkflowCallbacks; } ): Promise ``` #### Parameters | Parameter | Type | Required | Description | | ------------------------ | ------------------- | -------- | ---------------------------------------------------------------- | | `originalBorrowId` | `string` | Yes | Loan ID to repay | | `repayAmount` | `string` | Yes | Amount to repay | | `chain` | `ChainType` | No | Optional override. Inferred from the original loan when omitted. | | `useCollateral` | `boolean` | No | Use collateral for repayment | | `collateralToWithdraw` | `string` | No | BTC to withdraw | | `userBtcWithdrawAddress` | `string` | No | BTC destination | | `trackWorkflow` | `boolean` | No | Track the workflow | | `callbacks` | `WorkflowCallbacks` | No | Tracking callbacks | #### Example ```typescript theme={null} await sdk.repay(loanId, '1000', { collateralToWithdraw: '0.01', userBtcWithdrawAddress: 'bc1q...', trackWorkflow: true, callbacks: { onComplete: () => console.log('Repaid!') } }); ``` *** ### withdrawCollateral() Withdraw collateral from a loan. The SDK prepares the platform wallet address/signature automatically when needed; callers do not need to call `setup()` first. The loan chain is inferred from the original borrow transaction; pass `options.chain` only as an explicit override. ```typescript theme={null} async withdrawCollateral( originalBorrowId: string, collateralAmount: string, btcWithdrawAddress: string, options?: { chain?: ChainType; trackWorkflow?: boolean; callbacks?: WorkflowCallbacks; } ): Promise ``` #### Example ```typescript theme={null} await sdk.withdrawCollateral( loanId, '0.01', 'bc1q...', { trackWorkflow: true, callbacks } ); ``` *** ### borrowMore() Increase the debt on an existing loan without adding collateral. The original loan's collateral, market context, bridge and destination are reused server-side, so only the additional borrow amount is required. The borrowed funds are disbursed to the loan's configured destination. The SDK prepares the wallet session automatically; the loan chain is inferred from the original loan (pass `options.chain` only to override). ```typescript theme={null} async borrowMore( originalBorrowId: string, borrowAmount: string, options?: { chain?: ChainType; disbursementFeeBps?: number; trackWorkflow?: boolean; callbacks?: WorkflowCallbacks; } ): Promise ``` #### Parameters | Parameter | Type | Required | Description | | -------------------- | ------------------- | -------- | ---------------------------------------------- | | `originalBorrowId` | `string` | Yes | ID of the original borrow transaction | | `borrowAmount` | `string` | Yes | Additional amount to borrow, in the loan asset | | `disbursementFeeBps` | `number` | No | Payout fee override in basis points (0–10000) | | `trackWorkflow` | `boolean` | No | Track the workflow via callbacks | | `callbacks` | `WorkflowCallbacks` | No | Workflow tracking callbacks | #### Returns `BorrowMoreResult` — includes `transactionId`, `workflowId`, `status`, `borrowAmount`, `borrowerPayoutAmount`, `borrowAsset`, and destination details. Borrow More is rejected when the loan is fully repaid, liquidated, or has no remaining borrow room. The requested amount must not exceed the position's available borrow capacity. #### Example ```typescript theme={null} const result = await sdk.borrowMore(loanId, '500', { trackWorkflow: true, callbacks: { onStatusUpdate: (status) => console.log(status.label), onComplete: () => console.log('Additional funds disbursed!') } }); console.log(result.transactionId); ``` *** ### depositMore() Add collateral to an existing loan without borrowing. The new collateral is bridged from Bitcoin and deposited into the same position, improving the loan's health factor. No new debt is taken. The SDK prepares the wallet session automatically; the loan chain is inferred from the original loan. ```typescript theme={null} async depositMore( originalBorrowId: string, collateralAmount: string, options?: { chain?: ChainType; trackWorkflow?: boolean; callbacks?: WorkflowCallbacks; } ): Promise ``` #### Parameters | Parameter | Type | Required | Description | | ------------------ | ------------------- | -------- | --------------------------------------------------------------------------- | | `originalBorrowId` | `string` | Yes | ID of the original borrow transaction | | `collateralAmount` | `string` | Yes | Additional collateral to deposit, in BTC | | `trackWorkflow` | `boolean` | No | Track the workflow via callbacks | | `callbacks` | `WorkflowCallbacks` | No | Workflow tracking callbacks (includes `onDepositReady` for the BTC deposit) | #### Returns `DepositMoreResult` — includes `transactionId`, `workflowId`, `status`, `depositAmount`, `collateralAsset`, and `protocol`. #### Example ```typescript theme={null} const result = await sdk.depositMore(loanId, '0.01', { trackWorkflow: true, callbacks: { onDepositReady: (info) => console.log(`Send ${info.amountBTC} BTC to ${info.address}`), onComplete: () => console.log('Collateral added!') } }); ``` *** ### getLoanCollateralInfo() Get collateral information for a loan. ```typescript theme={null} async getLoanCollateralInfo(loanId: string): Promise ``` #### Returns ```typescript theme={null} { totalCollateral: string; availableCollateral: string; maxWithdrawable: string; totalDebt: string; remainingDebt: string; } ``` *** ### getLoanHistory() Get loan transaction history. ```typescript theme={null} async getLoanHistory(options?: { page?: number; limit?: number; status?: 'active' | 'pending' | 'all'; }): Promise> ``` #### Example ```typescript theme={null} const history = await sdk.getLoanHistory({ page: 1, limit: 20, status: 'active' }); ``` *** ### getPendingLoans() Get loans awaiting deposit. ```typescript theme={null} async getPendingLoans(): Promise ``` *** ### getRepayTransactions() Get repay transactions for a loan. ```typescript theme={null} async getRepayTransactions(loanId?: string): Promise ``` *** ### getRepayStatus() Get status of a repay transaction. ```typescript theme={null} async getRepayStatus(transactionId: string): Promise ``` *** ### trackWorkflow() Manually track a workflow. ```typescript theme={null} async trackWorkflow( workflowId: string, callbacks: WorkflowCallbacks, workflowType?: 'borrow' | 'repay' ): Promise ``` *** ### resumeLoan() Resume tracking a loan workflow. ```typescript theme={null} async resumeLoan(workflowId: string, callbacks?: WorkflowCallbacks): Promise ``` *** ### getStatus() Get workflow status. ```typescript theme={null} async getStatus(workflowId: string): Promise ``` *** ### sendBitcoin() Send Bitcoin (requires wallet provider with sendBitcoin). ```typescript theme={null} async sendBitcoin(toAddress: string, satoshis: number): Promise ``` *** ### getFees() Get bridge fee information for a given chain and collateral amount. Lightweight; use this for an early-stage estimate when you only know the destination chain and collateral. ```typescript theme={null} async getFees(params: FeesRequest): Promise ``` #### Example ```typescript theme={null} const fees = await sdk.getFees({ chain: ChainType.ARBITRUM, collateralAmount: '0.1' }); ``` *** ### getQuoteFees() Get the full fee breakdown for a specific quote: bridge fees plus borrow-side fees (platform fee, applied disbursement bps, campaign waiver, net loan amount). Call this after `getQuotes()` to display the final numbers a user will see at checkout. The borrow-side fees are returned separately from the quote because they depend on the borrower's campaign eligibility and the chosen loan asset/chain. ```typescript theme={null} async getQuoteFees(params: QuoteFeesRequestData): Promise ``` #### Parameters | Parameter | Type | Required | Description | | ------------------ | ----------- | -------- | ----------------------------------------------- | | `collateralAmount` | `string` | Yes | Collateral in BTC, e.g. `"0.1"` | | `loanAmount` | `string` | Yes | Loan amount in the loan asset's units | | `fromChain` | `ChainType` | Yes | Source chain of the collateral being bridged | | `fromAssetSymbol` | `string` | Yes | Source asset symbol (e.g. `BTC`) | | `toChain` | `ChainType` | Yes | Bridge destination chain | | `toAssetSymbol` | `string` | Yes | Bridge destination asset (e.g. `WBTC`, `cbBTC`) | | `loanChain` | `ChainType` | Yes | Chain on which the loan is issued | | `loanAssetSymbol` | `string` | Yes | Loan asset (e.g. `USDC`) | #### Example ```typescript theme={null} const quotes = await sdk.getQuotes({ collateralAmount: '0.1', loanAmount: '5000' }); const best = quotes[0]; const fees = await sdk.getQuoteFees({ collateralAmount: '0.1', loanAmount: '5000', fromChain: ChainType.BITCOIN, fromAssetSymbol: 'BTC', toChain: best.chain, toAssetSymbol: 'WBTC', loanChain: best.chain, loanAssetSymbol: 'USDC', }); console.log('Bridge fee USD:', fees.bridgeFees.totalBridgeFeeUSD); console.log('Platform fee:', fees.borrowFees.platformFee); console.log('Net loan to user:', fees.borrowFees.netLoanAmount); if (fees.borrowFees.feeSource === 'campaign') { console.log('Campaign discount applied:', fees.borrowFees.campaignSlug); } ``` For best UX, fetch quotes and quote fees in parallel once the user has entered both a collateral amount and a loan amount: ```typescript theme={null} const [quotes, fees] = await Promise.all([ sdk.getQuotes({ collateralAmount, loanAmount }), sdk.getQuoteFees({ collateralAmount, loanAmount, fromChain: ChainType.BITCOIN, fromAssetSymbol: 'BTC', toChain: selectedChain, toAssetSymbol: 'WBTC', loanChain: selectedChain, loanAssetSymbol: 'USDC', }), ]); ``` *** ### getWalletPositions() Get token positions for the platform wallet. ```typescript theme={null} async getWalletPositions(params?: { filterPositions?: string; filterTrash?: string; }): Promise ``` *** ### getWalletPortfolio() Get portfolio summary. ```typescript theme={null} async getWalletPortfolio(filter?: string): Promise ``` *** ### withdrawToBitcoin() Withdraw EVM assets to Bitcoin. The SDK prepares the platform wallet address/signature automatically when needed. ```typescript theme={null} async withdrawToBitcoin(params: WithdrawToBitcoinRequest): Promise ``` #### Parameters | Parameter | Type | Description | | ------------- | ----------- | ------------------------- | | `chain` | `ChainType` | Source chain | | `amount` | `string` | Amount to withdraw | | `assetSymbol` | `string` | Asset symbol (USDC, USDT) | | `btcAddress` | `string` | Destination BTC address | *** ### getWithdrawStatus() Get withdrawal status. ```typescript theme={null} async getWithdrawStatus(transactionId: string): Promise ``` *** ### withdrawToEVM() Withdraw USDC from the platform smart account to an EVM address with sponsored gas (gasless). The SDK prepares the platform wallet address/signature automatically when needed. ```typescript theme={null} async withdrawToEVM(params: WithdrawToEVMRequest): Promise ``` #### Parameters | Parameter | Type | Required | Description | | -------------------- | ----------- | -------- | --------------------------------------------- | | `chain` | `ChainType` | Yes | Source chain (ARBITRUM, BASE) | | `amount` | `string` | Yes | Amount to withdraw (e.g., '100' for 100 USDC) | | `assetSymbol` | `string` | No | Source asset symbol; defaults to USDC | | `destinationAddress` | `string` | Yes | Destination EVM address | #### Returns `string` - Asynchronous withdrawal transaction ID. Pass it to `getWithdrawStatus()` to obtain workflow progress and the final transaction hash. #### Features * **Gasless**: Transaction fees are sponsored via ZeroDev paymaster * **Same-chain**: Executes a token transfer on the selected source chain * **Asset-aware**: Supports assets configured for the selected chain #### Example ```typescript theme={null} // Withdraw USDC to your personal wallet (gasless) const transactionId = await sdk.withdrawToEVM({ chain: ChainType.ARBITRUM, amount: '100', destinationAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f...' }); const status = await sdk.getWithdrawStatus(transactionId); console.log('Transaction hash:', status.data.transactionDetails?.transactionHash); // Verify on block explorer: https://arbiscan.io/tx/{txHash} ``` *** ### clearSession() Clear session and reset state. ```typescript theme={null} clearSession(): void ``` *** ## Helper Function ### useBorrow() Factory function to create SDK instance. ```typescript theme={null} function useBorrow(config: BorrowSDKConfig): BorrowSDK ``` #### Example ```typescript theme={null} import { useBorrow } from '@satsterminal-sdk/borrow'; const sdk = useBorrow({ apiKey: 'your-api-key', wallet: walletProvider }); ``` # Errors Source: https://developer.satsterminal.com/borrow/api-reference/errors # Errors The SDK provides typed error classes for precise error handling. ## Error Hierarchy ``` Error └── BorrowSDKError (base class) ├── WalletNotConnectedError ├── SmartAccountError ├── ApiError ├── ConfigValidationError ├── QuoteError └── WorkflowError ``` ## BorrowSDKError Base error class for all SDK errors. ```typescript theme={null} class BorrowSDKError extends Error { public readonly code: ErrorCode; public readonly context?: Record; constructor( message: string, code?: ErrorCode, context?: Record ); toJSON(): object; } ``` ### Properties | Property | Type | Description | | --------- | ----------- | ------------------------ | | `code` | `ErrorCode` | Error classification | | `context` | `object` | Additional error context | | `message` | `string` | Error message | ### Example ```typescript theme={null} try { const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, callbacks, 'borrow'); } catch (error) { if (error instanceof BorrowSDKError) { console.log('Code:', error.code); console.log('Message:', error.message); console.log('Context:', error.context); console.log('JSON:', error.toJSON()); } } ``` *** ## WalletNotConnectedError Thrown when a wallet operation is attempted without a connected wallet. ```typescript theme={null} class WalletNotConnectedError extends BorrowSDKError { constructor(message?: string); } ``` ### Default Code `ErrorCode.WALLET_NOT_CONNECTED` ### Common Causes * Calling methods before wallet is connected * Wallet disconnected during operation * Missing wallet provider in config ### Example ```typescript theme={null} try { await sdk.setup(); } catch (error) { if (error instanceof WalletNotConnectedError) { console.error('Please connect your wallet first'); showConnectWalletModal(); } } ``` *** ## SmartAccountError Thrown when smart account operations fail. ```typescript theme={null} class SmartAccountError extends BorrowSDKError { constructor(message: string, cause?: unknown); } ``` ### Default Code `ErrorCode.SMART_ACCOUNT_ERROR` ### Common Causes * Signature verification failed * Smart account derivation failed * Session authorization failed * Account not initialized ### Example ```typescript theme={null} try { await sdk.startNewLoan(); } catch (error) { if (error instanceof SmartAccountError) { console.error('Smart account error:', error.message); // Check if it's a session issue if (error.message.includes('session')) { await sdk.setup(); // Re-initialize session } } } ``` *** ## ApiError Thrown when API requests fail. ```typescript theme={null} class ApiError extends BorrowSDKError { public readonly statusCode?: number; constructor( message: string, statusCode?: number, context?: Record ); static fromResponse( statusCode: number, message?: string, body?: any ): ApiError; } ``` ### Default Code `ErrorCode.API_ERROR` ### Properties | Property | Type | Description | | ------------ | -------- | ---------------- | | `statusCode` | `number` | HTTP status code | ### Common Status Codes | Code | Meaning | | ---- | ------------------------------ | | 400 | Bad request / validation error | | 401 | Unauthorized / invalid API key | | 403 | Forbidden | | 404 | Resource not found | | 429 | Rate limited | | 500 | Server error | ### Example ```typescript theme={null} try { await sdk.getQuotes({ ... }); } catch (error) { if (error instanceof ApiError) { switch (error.statusCode) { case 401: console.error('Invalid API key'); break; case 429: console.error('Rate limited. Please wait.'); await delay(5000); // Retry break; case 500: console.error('Server error. Please try again later.'); break; default: console.error('API error:', error.message); } } } ``` *** ## ConfigValidationError Thrown when SDK configuration is invalid. ```typescript theme={null} class ConfigValidationError extends BorrowSDKError { constructor(message: string); } ``` ### Default Code `ErrorCode.CONFIG_ERROR` ### Common Causes * Missing required config options * Invalid config values * Invalid URL format * Invalid chain type ### Example ```typescript theme={null} try { const sdk = new BorrowSDK({ apiKey: '', // Empty - will throw wallet: { address: 'bc1...' } // Missing signMessage - will throw }); } catch (error) { if (error instanceof ConfigValidationError) { console.error('Configuration error:', error.message); // e.g., "apiKey is required and must be a non-empty string" } } ``` ### Validation Rules | Option | Validation | | ------------------------ | ---------------- | | `apiKey` | Non-empty string | | `baseUrl` | Valid URL | | `chain` | Valid ChainType | | `wallet.address` | Non-empty string | | `wallet.signMessage` | Function | | `workflowPollInterval` | >= 100 | | `sessionValiditySeconds` | >= 60 | *** ## QuoteError Thrown when quote operations fail. ```typescript theme={null} class QuoteError extends BorrowSDKError { constructor(message: string, context?: Record); } ``` ### Default Code `ErrorCode.QUOTE_ERROR` ### Common Causes * No quotes available for parameters * Invalid quote parameters * Quote expired ### Example ```typescript theme={null} try { await sdk.getQuotes({ collateralAmount: '0.001', // Too small loanAmount: '100000' // Too large }); } catch (error) { if (error instanceof QuoteError) { console.error('No quotes available:', error.message); showMessage('Please adjust your loan parameters'); } } ``` *** ## WorkflowError Thrown when workflow operations fail. ```typescript theme={null} class WorkflowError extends BorrowSDKError { public readonly workflowId?: string; constructor( message: string, workflowId?: string, context?: Record ); } ``` ### Default Code `ErrorCode.WORKFLOW_ERROR` ### Properties | Property | Type | Description | | ------------ | -------- | ------------------ | | `workflowId` | `string` | Failed workflow ID | ### Common Causes * Workflow timeout * Deposit not received * Bridge failure * Execution failure ### Example ```typescript theme={null} try { await sdk.trackWorkflow(workflowId, callbacks); } catch (error) { if (error instanceof WorkflowError) { console.error('Workflow failed:', error.message); console.error('Workflow ID:', error.workflowId); // Check status const status = await sdk.getStatus(error.workflowId!); console.log('Final status:', status); } } ``` *** ## Error Handling Patterns ### Comprehensive Handler ```typescript theme={null} async function handleSDKOperation( operation: () => Promise ): Promise { try { return await operation(); } catch (error) { if (error instanceof WalletNotConnectedError) { showModal('Please connect your wallet'); return null; } if (error instanceof SmartAccountError) { if (error.message.includes('session')) { // Try to refresh session await sdk.setup(); return await operation(); // Retry } showError('Wallet error: ' + error.message); return null; } if (error instanceof ApiError) { if (error.statusCode === 429) { showError('Too many requests. Please wait.'); await delay(5000); return await operation(); // Retry } showError('Server error: ' + error.message); return null; } if (error instanceof ConfigValidationError) { console.error('Config error:', error.message); throw error; // Fatal - can't recover } if (error instanceof QuoteError) { showError('No quotes available. Try different parameters.'); return null; } if (error instanceof WorkflowError) { showError('Operation failed: ' + error.message); // Log for debugging console.error('Workflow ID:', error.workflowId); return null; } // Unknown error console.error('Unexpected error:', error); showError('An unexpected error occurred'); return null; } } ``` ### Type Guard ```typescript theme={null} function isBorrowSDKError(error: unknown): error is BorrowSDKError { return error instanceof BorrowSDKError; } // Usage if (isBorrowSDKError(error)) { console.log('SDK Error:', error.code, error.message); } ``` ### Error Logging ```typescript theme={null} function logError(error: BorrowSDKError) { const errorData = { code: error.code, message: error.message, context: error.context, stack: error.stack, timestamp: new Date().toISOString() }; // Send to logging service analytics.trackError(errorData); // Log locally console.error('SDK Error:', JSON.stringify(errorData, null, 2)); } ``` *** ## ErrorCode Reference ```typescript theme={null} enum ErrorCode { WALLET_NOT_CONNECTED = 'WALLET_NOT_CONNECTED', SMART_ACCOUNT_ERROR = 'SMART_ACCOUNT_ERROR', API_ERROR = 'API_ERROR', CONFIG_ERROR = 'CONFIG_ERROR', QUOTE_ERROR = 'QUOTE_ERROR', WORKFLOW_ERROR = 'WORKFLOW_ERROR', STORAGE_ERROR = 'STORAGE_ERROR', NETWORK_ERROR = 'NETWORK_ERROR', VALIDATION_ERROR = 'VALIDATION_ERROR' } ``` ### Usage ```typescript theme={null} import { ErrorCode, BorrowSDKError } from '@satsterminal-sdk/borrow'; if (error instanceof BorrowSDKError) { switch (error.code) { case ErrorCode.WALLET_NOT_CONNECTED: // Handle wallet not connected break; case ErrorCode.API_ERROR: // Handle API error break; // ... etc } } ``` # Types Source: https://developer.satsterminal.com/borrow/api-reference/types # Types Complete type reference for the SatsTerminal Borrow SDK. ## Enums ### ChainType Supported blockchain networks. ```typescript theme={null} enum ChainType { BASE = 'BASE', BITCOIN = 'BITCOIN', ETHEREUM = 'ETHEREUM', POLYGON = 'POLYGON', OPTIMISM = 'OPTIMISM', ARBITRUM = 'ARBITRUM', BSC = 'BNB_SMART_CHAIN', BASE_SEPOLIA = 'BASE_SEPOLIA', HYPERLIQUID = 'HYPERLIQUID-PERPS', LIGHTER = 'LIGHTER' } ``` ### SessionScope Session permission levels. ```typescript theme={null} enum SessionScope { READ = 'read', DEFI = 'defi', FULL = 'full' } ``` ### ErrorCode Error classification codes. ```typescript theme={null} enum ErrorCode { WALLET_NOT_CONNECTED = 'WALLET_NOT_CONNECTED', SMART_ACCOUNT_ERROR = 'SMART_ACCOUNT_ERROR', API_ERROR = 'API_ERROR', CONFIG_ERROR = 'CONFIG_ERROR', QUOTE_ERROR = 'QUOTE_ERROR', WORKFLOW_ERROR = 'WORKFLOW_ERROR', STORAGE_ERROR = 'STORAGE_ERROR', NETWORK_ERROR = 'NETWORK_ERROR', VALIDATION_ERROR = 'VALIDATION_ERROR' } ``` *** ## Configuration ### BorrowSDKConfig SDK configuration options. ```typescript theme={null} interface BorrowSDKConfig { // Required apiKey: string; wallet: WalletProvider; // Optional rpcUrl?: string; bundlerUrl?: string; storage?: StorageProvider; workflowPollInterval?: number; sessionValiditySeconds?: number; autoTrackWorkflows?: boolean; quoteSelector?: (quotes: Quote[]) => Quote; retryConfig?: RetryConfig; logger?: Logger; } ``` ### WalletProvider Wallet integration interface. ```typescript theme={null} interface WalletProvider { address: string; publicKey?: string; signMessage: (message: string) => Promise; sendBitcoin?: (toAddress: string, satoshis: number) => Promise; } ``` ### StorageProvider Storage abstraction interface. ```typescript theme={null} interface StorageProvider { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem(key: string): void; clear(): void; } ``` ### RetryConfig Request retry configuration. ```typescript theme={null} interface RetryConfig { maxRetries: number; retryDelay: number; retryableStatusCodes: number[]; } ``` ### Logger Logging interface. ```typescript theme={null} interface Logger { debug: (message: string) => void; info: (message: string) => void; warn: (message: string) => void; error: (message: string) => void; } ``` *** ## User & Session ### UserStatus Current user status. ```typescript theme={null} interface UserStatus { isConnected: boolean; btcAddress?: string; smartAccountAddress?: string; isDeployed?: boolean; hasActiveSession: boolean; sessionExpiry?: number; } ``` ### ActiveSession Active session details. ```typescript theme={null} interface ActiveSession { sessionKeyAddress?: string; sessionPrivateKey?: string; validUntil: number; authorizationSignature?: string; scope?: SessionScope; } ``` *** ## Loan & Quote ### QuoteRequest Parameters for requesting quotes. ```typescript theme={null} interface QuoteRequest { collateralAmount: string; loanAmount: string; filter?: { chains?: ChainType[]; excludeChains?: ChainType[]; }; } ``` ### Quote Loan quote from a protocol. Use [`getQuoteFees()`](/borrow/api-reference/borrow-sdk#getquotefees) to fetch the fee breakdown for a selected quote. ```typescript theme={null} interface Quote { collateralAmount: string; loanAmount: string; protocol: string; chain: ChainType; /** Chain on which the loan is issued. */ loanChain?: ChainType; /** Source chain of the bridged collateral. */ sourceChain?: ChainType; /** Bridge-destination asset symbol (e.g. "WBTC", "cbBTC", "BTCB"). Required when building `QuoteFeesRequestData`. */ collateralAssetSymbol?: string; /** Loan asset symbol (e.g. "USDC"). Required when building `QuoteFeesRequestData`. */ loanAssetSymbol?: string; borrowApy: { variable: string; stable: string; }; effectiveApy?: { variable: string; stable: string; }; } ``` ### LoanCollateralInfo Collateral information for a loan. ```typescript theme={null} interface LoanCollateralInfo { totalCollateral: string; availableCollateral: string; maxWithdrawable: string; totalDebt: string; remainingDebt: string; } ``` *** ## Webhooks ### SdkWebhookEventType ```typescript theme={null} enum SdkWebhookEventType { BTC_DEPOSIT_CONFIRMED = 'sdk.borrow.btc_deposit_confirmed' } ``` ### SdkWebhookConfig ```typescript theme={null} interface SdkWebhookConfig { enabled: boolean; url?: string; events: SdkWebhookEventType[]; createdAt?: string | Date; updatedAt?: string | Date; secretLastRotatedAt?: string | Date; } ``` ### SdkWebhookConfigUpdateRequest ```typescript theme={null} interface SdkWebhookConfigUpdateRequest { enabled?: boolean; url?: string; events?: SdkWebhookEventType[]; } ``` ### SdkWebhookSecretResponse ```typescript theme={null} interface SdkWebhookSecretResponse { config: SdkWebhookConfig; secret: string; } ``` ### SdkBtcDepositConfirmedWebhookPayload ```typescript theme={null} interface SdkBtcDepositConfirmedWebhookPayload { id: string; type: SdkWebhookEventType.BTC_DEPOSIT_CONFIRMED; apiVersion: string; createdAt: string; data: { transactionId: string; workflowId?: string; status: 'DEPOSIT_CONFIRMED'; btcDepositTxHash?: string | null; bridgeRedeemTxHash?: string | null; depositAddress?: string | null; }; } ``` *** ## Transactions ### UserTransaction User transaction record. ```typescript theme={null} interface UserTransaction { id: string; type: 'borrow' | 'repay' | 'withdraw'; status: 'pending' | 'active' | 'completed' | 'failed' | 'awaiting_deposit'; amount: string; borrowPrincipalAmount?: string; borrowerPayoutAmount?: string; platformFeeAmount?: string; feeSnapshot?: BorrowFeeSnapshot; settlementLegs?: BorrowSettlementLeg[]; currency: string; txHash?: string; timestamp: number; borrowTransaction?: any; repayTransaction?: any; } ``` ### BorrowSettlementLegStatus Status of a borrow settlement leg. ```typescript theme={null} type BorrowSettlementLegStatus = 'pending' | 'completed' | 'failed'; ``` ### BorrowFeeDistributionStatus High-level outcome of fee distribution. ```typescript theme={null} type BorrowFeeDistributionStatus = 'pending' | 'completed' | 'failed'; ``` ### BorrowFeeSettlementMode How the borrow fee is settled to treasury. ```typescript theme={null} type BorrowFeeSettlementMode = 'directToBaseTreasury'; ``` ### BorrowFeeBridgeSource Which user-owned wallet funded the fee settlement. ```typescript theme={null} type BorrowFeeBridgeSource = 'evmLoanWallet' | 'solanaLoanWallet'; ``` ### BorrowFeeBridgeStatus Progress of the fee settlement transfer or bridge. ```typescript theme={null} type BorrowFeeBridgeStatus = 'pending' | 'submitted' | 'settled' | 'failed'; ``` ### BorrowFeePayoutStatus Progress of the treasury payout to the referrer. ```typescript theme={null} type BorrowFeePayoutStatus = 'pending' | 'submitted' | 'completed' | 'failed'; ``` ### BorrowFeeBridgeStatusContext Persisted context used to resume LI.FI bridge status checks. ```typescript theme={null} interface BorrowFeeBridgeStatusContext { txHash: string; bridge?: string; fromChain: number; toChain: number; fallbackRawAmount: string; } ``` ### BorrowSettlementLeg Settlement leg recorded on a borrow transaction. ```typescript theme={null} interface BorrowSettlementLeg { kind: 'feeDebit' | string; address: string; asset: string; chain: ChainType | string; amount: string; txHash?: string; status: BorrowSettlementLegStatus; } ``` ### BorrowFeeSnapshot Snapshot of borrow fee collection, settlement, and referral payout. ```typescript theme={null} interface BorrowFeeSnapshot { borrowPrincipalAmount: string; borrowerPayoutAmount: string; disbursementFeeBps: number; disbursementFeeAmount: string; platformFeeAmount: string; referralFeeAmount: string; referrerFeePercentAtOrigination?: number; feeAsset: string; feeTokenAddress: string; feeChain: ChainType | string; feeTreasuryAddress: string; referrerPayoutAddress?: string; settlementMode?: BorrowFeeSettlementMode; bridgeSource?: BorrowFeeBridgeSource; bridgeProvider?: string; bridgeStatus?: BorrowFeeBridgeStatus; bridgeFeeUsd?: string; settlementAmount?: string; platformPayoutAmount?: string; referralPayoutAmount?: string; feeBridgeTxHash?: string; feeBridgeDestinationTxHash?: string; referralPayoutTxHash?: string; payoutStatus?: BorrowFeePayoutStatus; distributionStatus?: BorrowFeeDistributionStatus; distributionError?: string; bridgeStatusContext?: BorrowFeeBridgeStatusContext; } ``` ### PaginatedResponse Paginated response wrapper. ```typescript theme={null} interface PaginatedResponse { transactions: T[]; pagination: { totalTransactions: number; currentPage: number; limit: number; totalPages: number; hasNext: boolean; hasPrevious: boolean; }; } ``` ### RepayTransaction Repayment transaction details. ```typescript theme={null} interface RepayTransaction { _id: string; userId: string; originalBorrowId: string; loanChainAddress: string; repayAmount: string; withdrawalCollateralAmount: string; repayTokenAddress: string; isPartialRepay: boolean; isPartialWithdrawCollateral: boolean; useCollateral: boolean; workflowId?: string; transactionStatuses: Array<{ status: string; timestamp: Date; details?: string; }>; repayConfig: any; userBtcWithdrawAddress?: string; bridgeConfig?: any; workflowState?: RepayWorkflowState; createdAt: Date; updatedAt: Date; } ``` ### RepayTransactionStatusResponse Status response for repay transactions. ```typescript theme={null} interface RepayTransactionStatusResponse { transactionId?: string; workflowId?: string; status?: string; stage?: string; transactionStatuses?: RepayTransaction['transactionStatuses']; transactionDetails: Partial; workflowState?: RepayWorkflowState; transactionState?: RepayWorkflowState; } ``` *** ## Workflow ### BorrowWorkflowStage Stages shared by Borrow, Borrow More, and Deposit More. Each operation visits only the stages relevant to its flow. ```typescript theme={null} enum BorrowWorkflowStage { INITIALIZING = 'INITIALIZING', QUOTE_READY = 'QUOTE_READY', DEPOSIT_ADDRESS_READY = 'DEPOSIT_ADDRESS_READY', PREPARING_DEPOSIT = 'PREPARING_DEPOSIT', AWAITING_DEPOSIT = 'AWAITING_DEPOSIT', AUTO_TRANSFERRING_BTC = 'AUTO_TRANSFERRING_BTC', AWAITING_DEPOSIT_CONFIRMATION = 'AWAITING_DEPOSIT_CONFIRMATION', DEPOSIT_CONFIRMED = 'DEPOSIT_CONFIRMED', AWAITING_REDEEM = 'AWAITING_REDEEM', REDEEM_CONFIRMED = 'REDEEM_CONFIRMED', PREPARING_BORROW = 'PREPARING_BORROW', PREPARING_BORROW_DEPOSIT = 'PREPARING_BORROW_DEPOSIT', COLLATERAL_DEPOSITED = 'COLLATERAL_DEPOSITED', PREPARING_LOAN = 'PREPARING_LOAN', LOAN_CONFIRMED = 'LOAN_CONFIRMED', PREPARING_DISBURSEMENT = 'PREPARING_DISBURSEMENT', DISBURSEMENT_SUBMITTED = 'DISBURSEMENT_SUBMITTED', DISBURSEMENT_COMPLETED = 'DISBURSEMENT_COMPLETED', DISBURSEMENT_FAILED = 'DISBURSEMENT_FAILED', PREPARING_REPAY = 'PREPARING_REPAY', AWAITING_REPAY_CONFIRMATION = 'AWAITING_REPAY_CONFIRMATION', PREPARING_WITHDRAW = 'PREPARING_WITHDRAW', AWAITING_WITHDRAW_CONFIRMATION = 'AWAITING_WITHDRAW_CONFIRMATION', COMPLETED = 'COMPLETED', FAILED = 'FAILED', CANCELLED = 'CANCELLED', REFUND_INITIATED = 'REFUND_INITIATED', REFUND_COMPLETED = 'REFUND_COMPLETED' } ``` Import it from the Borrow SDK: ```typescript theme={null} import { BorrowWorkflowStage } from '@satsterminal-sdk/borrow'; ``` ### WorkflowStatus Current workflow status. ```typescript theme={null} interface WorkflowStatus { stage: string; step: number; label: string; description: string; depositAddress?: string; depositAmount?: number; depositAmountBTC?: number; depositTxHash?: string; currentConfirmations?: number; requiredConfirmations?: number; error?: string; isComplete: boolean; isFailed: boolean; rawData: any; } ``` ### WorkflowCallbacks Callbacks for workflow tracking. ```typescript theme={null} interface WorkflowCallbacks { onStatusUpdate?: (status: WorkflowStatus) => void; onComplete?: (result: any) => void; onError?: (error: string) => void; onDepositReady?: (info: DepositInfo) => void; } ``` ### DepositInfo Deposit information. ```typescript theme={null} interface DepositInfo { address: string; amount: number; amountBTC: number; txHash?: string; } ``` ### MoreTransactionStatusResponse Status returned by `getBorrowMoreStatus()` and `getDepositMoreStatus()`. ```typescript theme={null} interface MoreTransactionStatusResponse { transactionId: string; workflowId?: string; status?: string; stage?: string; originalBorrowId?: string; transactionStatuses?: Array<{ status: string; timestamp: string | Date; details?: string; }>; transactionState?: { stage?: string; depositAddress?: string | null; depositAmount?: string | null; // satoshis depositTxHash?: string | null; currentConfirmations?: number | null; requiredConfirmations?: number | null; error?: string | null; }; /** @deprecated Use transactionState. */ workflowState?: MoreTransactionStatusResponse['transactionState']; transactionDetails?: Record; } ``` ### RepayWorkflowStage Repay workflow stages. ```typescript theme={null} enum RepayWorkflowStage { INITIALIZING = 'INITIALIZING', TRANSFERRING_TO_KERNEL = 'TRANSFERRING_TO_KERNEL', WITHDRAWING_COLLATERAL = 'WITHDRAWING_COLLATERAL', REPAYING_LOAN = 'REPAYING_LOAN', TRANSFERRING_TO_PLATFORM_WALLET = 'TRANSFERRING_TO_PLATFORM_WALLET', BRIDGE_INITIALIZING = 'BRIDGE_INITIALIZING', BRIDGE_QUOTE_READY = 'BRIDGE_QUOTE_READY', BRIDGE_SWAP_CREATED = 'BRIDGE_SWAP_CREATED', BRIDGE_EXECUTING_APPROVAL = 'BRIDGE_EXECUTING_APPROVAL', BRIDGE_APPROVAL_CONFIRMED = 'BRIDGE_APPROVAL_CONFIRMED', BRIDGE_EXECUTING_INITIATE = 'BRIDGE_EXECUTING_INITIATE', BRIDGE_INITIATE_CONFIRMED = 'BRIDGE_INITIATE_CONFIRMED', BRIDGE_AWAITING_BRIDGE_COMPLETION = 'BRIDGE_AWAITING_BRIDGE_COMPLETION', BRIDGE_COMPLETED = 'BRIDGE_COMPLETED', COMPLETED = 'COMPLETED', FAILED = 'FAILED', CANCELLED = 'CANCELLED' } ``` ### RepayWorkflowStep Repay workflow steps. ```typescript theme={null} enum RepayWorkflowStep { TRANSFER_TO_KERNEL = 'transferToKernel', REPAY = 'repay', WITHDRAW = 'withdraw', REPAY_AND_WITHDRAW = 'repayAndWithdraw', TRANSFER_TO_PLATFORM_WALLET = 'transferToPlatformWallet', BRIDGE_COLLATERAL_TO_BITCOIN = 'bridgeCollateralToBitcoin' } ``` ### RepayWorkflowState Repay workflow state. ```typescript theme={null} interface RepayWorkflowState { stage: RepayWorkflowStage; currentStep?: RepayWorkflowStep; error?: string; transferToKernelTx?: string[]; withdrawCollateralTx?: string[]; transferToPlatformWalletTx?: string[]; repayTx?: string[]; transferAndBridgeCollateralAmount?: string; bridgeTx?: string[]; bridgeQuote?: any; bridgeSwap?: any; bridgeApprovalTxHash?: string; bridgeInitiateTxHash?: string; bridgeRedeemTxHash?: string; } ``` *** ## Wallet & Portfolio ### WalletPosition Token position in wallet. ```typescript theme={null} interface WalletPosition { id: string; type: string; attributes: { parent: string | null; protocol: string | null; name: string; position_type: string; quantity: { int: string; decimals: number; float: number; numeric: string; }; value: number | null; price: number; changes: { absolute_1d: number | null; percent_1d: number | null; } | null; fungible_info: { name: string; symbol: string; icon: { url: string } | null; flags: { verified: boolean }; implementations: Array<{ chain_id: string; address: string; decimals: number; }>; } | null; flags: { displayable: boolean; is_trash: boolean; }; updated_at: string; updated_at_block: number; }; relationships: { chain: { data: { type: string; id: string } }; }; } ``` ### WalletPositionsResponse Response for wallet positions. ```typescript theme={null} interface WalletPositionsResponse { data: WalletPosition[]; links?: { self: string }; } ``` ### WalletPortfolio Portfolio summary. ```typescript theme={null} interface WalletPortfolio { id: string; type: string; attributes: { positions_distribution_by_type: Record; positions_distribution_by_chain: Record; total: { positions: number }; changes: { absolute_1d: number; percent_1d: number; }; }; } ``` ### WalletPortfolioResponse Response for wallet portfolio. ```typescript theme={null} interface WalletPortfolioResponse { data: WalletPortfolio; links?: { self: string }; } ``` *** ## Fees & Withdrawals ### FeesRequest Fee information request. ```typescript theme={null} interface FeesRequest { chain: ChainType; collateralAmount: string; } ``` ### FeesResponseData Fee information response. ```typescript theme={null} interface FeesResponseData { chain: ChainType; collateralAmount: string; gardenFeePercent: number; affiliateFeePercent: number; totalBridgeFeePercentage: number; totalBridgeFeeUSD: string; gardenFeeUSD: string; affiliateFeeUSD: string; estimatedGasFee: string; } ``` ### QuoteFeesRequestData Request body for `BorrowSDK.getQuoteFees()`. ```typescript theme={null} interface QuoteFeesRequestData { /** Human-readable BTC amount, for example "0.1". */ collateralAmount: string; /** Loan amount in the loan asset's units, e.g. "5000" USDC. */ loanAmount: string; fromChain: ChainType; /** Source asset symbol, e.g. "BTC". */ fromAssetSymbol: string; toChain: ChainType; /** Bridge destination asset, e.g. "WBTC", "cbBTC". */ toAssetSymbol: string; loanChain: ChainType; /** Loan asset symbol, e.g. "USDC". */ loanAssetSymbol: string; } ``` ### BorrowQuoteFeesData Borrow-side fee breakdown for the requested loan. `feeSource` is `'campaign'` when a discount applies, otherwise `'default'`. ```typescript theme={null} interface BorrowQuoteFeesData { defaultDisbursementFeeBps: number; appliedDisbursementFeeBps: number; platformFee: string; netLoanAmount: string; waivedPlatformFee: string; feeSource: 'default' | 'campaign'; campaignSlug?: string; } ``` ### QuoteFeesResponseData Full fee response returned by `BorrowSDK.getQuoteFees()`. Combines bridge fees with the borrow-side fee breakdown. ```typescript theme={null} interface QuoteFeesResponseData { bridgeFees: FeesResponseData; borrowFees: BorrowQuoteFeesData; } ``` ### WithdrawToBitcoinRequest Cross-chain withdrawal request to Bitcoin. ```typescript theme={null} interface WithdrawToBitcoinRequest { chain: ChainType; amount: string; assetSymbol: string; btcAddress: string; } ``` ### WithdrawToEVMRequest Gasless EVM withdrawal request (sponsored gas). ```typescript theme={null} interface WithdrawToEVMRequest { chain: ChainType; amount: string; destinationAddress: string; /** Optional wallet index. Defaults to 0 (platform wallet where borrowed USDC goes). */ loanIndex?: number; } ``` ### WithdrawToEVMResponse Response from EVM withdrawal. ```typescript theme={null} interface WithdrawToEVMResponse { success: boolean; data: { transactionId: string; workflowId?: string; status: string; sourceAddress: string; destinationAddress: string; amount: string; chain: ChainType; }; } ``` ### WithdrawStatusResponse Withdrawal status response. ```typescript theme={null} interface WithdrawStatusResponse { success: boolean; data: { transactionId: string; workflowId?: string; type?: 'bitcoin' | 'evm-transfer' | 'collateral'; status: string; workflowState?: { stage: string; transactionHash?: string; approvalTxHash?: string; initiateTxHash?: string; redeemTxHash?: string; error?: string; }; transactionDetails?: { sendAmount?: string; receiveAmount?: string; sourceChain?: ChainType; destinationChain?: ChainType; sourceAddress?: string; destinationAddress?: string; sourceAsset?: string; destinationAsset?: string; transactionHash?: string; }; }; } ``` *** ## Utility Types ### Units Type-safe Bitcoin unit conversion utilities with branded types. ```typescript theme={null} // Branded types for type safety type Satoshis = number & { readonly __brand: 'satoshis' }; type BTC = string & { readonly __brand: 'btc' }; const Units = { // Conversions satsToBtc: (sats: number) => BTC; btcToSats: (btc: string) => Satoshis; // Formatting formatBtc: (btc: BTC | string, decimals?: number) => string; formatSats: (sats: Satoshis | number) => string; // Type constructors asBtc: (value: string) => BTC; asSatoshis: (value: number) => Satoshis; // Smart normalization (handles both sats and BTC inputs) normalizeToBtc: (value: string | number) => BTC; normalizeToSats: (value: string | number) => Satoshis; // Detection isLikelySatoshis: (value: number) => boolean; }; ``` #### Example ```typescript theme={null} import { Units, type Satoshis, type BTC } from '@satsterminal-sdk/borrow'; // Basic conversions Units.satsToBtc(10000000); // "0.10000000" (typed as BTC) Units.btcToSats("0.1"); // 10000000 (typed as Satoshis) // Smart normalization (auto-detects units) Units.normalizeToBtc(10000000); // "0.10000000" (detected as sats) Units.normalizeToBtc("0.1"); // "0.10000000" (detected as BTC) // Formatting for display Units.formatBtc("0.12345678", 4); // "0.1235" Units.formatSats(1234567); // "1,234,567" // Type-safe branded values const sats: Satoshis = Units.asSatoshis(10000000); const btc: BTC = Units.asBtc("0.1"); ``` *** ### ResponseNormalizer Utilities for handling various API response formats. ```typescript theme={null} const ResponseNormalizer = { // Normalize quote responses from various formats normalizeQuotes: (response: any) => Quote[]; // Normalize repay transaction responses normalizeRepayTransactions: (response: any) => RepayTransaction[]; // Extract loan collateral info from response extractLoanCollateralInfo: (response: any) => LoanCollateralInfo | null; // Normalize borrow transaction to canonical format normalizeBorrowTransaction: (raw: any) => NormalizedTransaction | null; }; ``` #### Example ```typescript theme={null} import { ResponseNormalizer } from '@satsterminal-sdk/borrow'; // Handle various quote response formats const quotes = ResponseNormalizer.normalizeQuotes(apiResponse); // Works with: data[], { allQuotes: [] }, { data: { allQuotes: [] } }, etc. // Extract collateral info regardless of response structure const collateralInfo = ResponseNormalizer.extractLoanCollateralInfo(loanData); ``` *** ### BtcUtils (Deprecated) Legacy Bitcoin utility class. Use `Units` instead for type-safe conversions. ```typescript theme={null} /** @deprecated Use Units instead */ class BtcUtils { static satsToBtc(sats: number): string; static btcToSats(btc: string): number; /** @deprecated Use Units.normalizeToBtc() instead */ static formatBtcDisplay(amount: string | number): string; } ``` #### Migration ```typescript theme={null} // Before (deprecated) import { BtcUtils } from '@satsterminal-sdk/borrow'; BtcUtils.satsToBtc(10000000); BtcUtils.formatBtcDisplay(amount); // After (recommended) import { Units } from '@satsterminal-sdk/borrow'; Units.satsToBtc(10000000); Units.normalizeToBtc(amount); ``` # Architecture Source: https://developer.satsterminal.com/borrow/core-concepts/architecture # Architecture Overview The SatsTerminal Borrow SDK is designed with separation of concerns and modularity in mind. ## High-Level Architecture ```mermaid theme={null} flowchart TD SDK["BorrowSDK
(Public API Facade)"] WM["WalletManager
(Wallets)"] LM["LoanManager
(Loans)"] WT["WorkflowTracker
(Tracking)"] API["ApiClient
(HTTP)"] STA["SatsTerminal API"] SDK --> WM SDK --> LM SDK --> WT WM --> API LM --> API WT --> API API --> STA ``` ## Components ### BorrowSDK (Facade) The main entry point that exposes the public API. It orchestrates the internal managers and provides a unified interface for all operations. ```typescript theme={null} const sdk = new BorrowSDK(config); const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, callbacks, 'borrow'); ``` ### WalletManager Handles smart wallet generation and management: * **Deterministic derivation** - Wallets derived from BTC signatures * **Multi-wallet support** - Each loan gets a unique wallet index * **Signature caching** - Persists signatures to avoid re-signing ### LoanManager Manages all loan-related operations: * Quote fetching * Borrow execution * Repayment processing * Collateral management * Transaction history ### WorkflowTracker Tracks asynchronous workflow status: * Polling-based status updates * Callback notifications * Automatic cleanup on completion ### ApiClient HTTP abstraction layer: * Request/response handling * Authentication headers * Error transformation ### StorageAdapter Storage abstraction with fallbacks: * Uses `localStorage` in browsers * Falls back to `MemoryStorage` in Node.js * Supports custom storage providers ## Data Flow ### Loan Creation Flow ```mermaid theme={null} sequenceDiagram participant User participant SDK participant API participant Backend User->>SDK: executeBorrow(selectedQuote) SDK->>API: startNewLoan() API->>Backend: create wallet Backend-->>API: wallet created API-->>SDK: loan wallet ready SDK->>API: getQuotes() API-->>SDK: quotes SDK->>API: executeBorrow() API->>Backend: start workflow Backend-->>API: workflow started API-->>SDK: workflow id API-->>SDK: deposit address (onDepositReady) SDK-->>User: deposit info User-->>Backend: deposit BTC loop poll status SDK->>API: get status API-->>SDK: status update end API-->>SDK: completed SDK-->>User: onComplete() ``` ## Module Dependencies ``` BorrowSDK ├── ApiClient ├── WalletManager │ ├── ApiClient │ └── StorageAdapter ├── LoanManager │ └── ApiClient └── WorkflowTracker └── ApiClient ``` ## Design Principles ### 1. Facade Pattern The `BorrowSDK` class acts as a facade, hiding the complexity of internal managers behind a simple API. ### 2. Dependency Injection Components receive their dependencies through constructors, making them testable and configurable. ### 3. Separation of Concerns Each manager handles a specific domain: * `WalletManager` - Wallet operations * `LoanManager` - Loan operations * `WorkflowTracker` - Async tracking ### 4. Graceful Degradation Storage falls back gracefully between `localStorage` and `MemoryStorage`. ### 5. Error Boundaries Typed errors with context enable precise error handling: ```typescript theme={null} try { const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, callbacks, 'borrow'); } catch (error) { if (error instanceof ApiError) { // Handle API errors } else if (error instanceof SmartAccountError) { // Handle wallet errors } } ``` ## File Structure # Sessions Source: https://developer.satsterminal.com/borrow/core-concepts/sessions # Sessions Sessions provide time-limited authorization for SDK operations without requiring repeated signatures. ## What is a Session? A session is an authorization token that allows the SDK to execute transactions on behalf of the user for a limited time. ```typescript theme={null} const { activeSession } = await sdk.setup(); console.log('Session key:', activeSession.sessionKeyAddress); console.log('Valid until:', new Date(activeSession.validUntil * 1000)); console.log('Scope:', activeSession.scope); ``` ## Session Properties | Property | Type | Description | | ------------------------ | -------------- | -------------------------- | | `sessionKeyAddress` | `string` | Address of the session key | | `validUntil` | `number` | Unix timestamp of expiry | | `scope` | `SessionScope` | Permission level | | `authorizationSignature` | `string` | Authorization proof | ## Session Scopes Sessions have different permission levels: ```typescript theme={null} import { SessionScope } from '@satsterminal-sdk/borrow'; SessionScope.READ // Read-only operations SessionScope.DEFI // DeFi operations (borrow, repay) SessionScope.FULL // All operations ``` ### Scope Permissions | Scope | Read Data | Borrow | Repay | Withdraw | Admin | | ------ | --------- | ------ | ----- | -------- | ----- | | `READ` | ✓ | ✗ | ✗ | ✗ | ✗ | | `DEFI` | ✓ | ✓ | ✓ | ✓ | ✗ | | `FULL` | ✓ | ✓ | ✓ | ✓ | ✓ | The SDK uses `DEFI` scope by default. ## Session Lifecycle ### 1. Creation Sessions are created during `setup()`: ```typescript theme={null} const { activeSession } = await sdk.setup(); // Session is now active ``` ### 2. Usage The SDK automatically uses the session for subsequent operations: ```typescript theme={null} // These use the active session await sdk.executeBorrow(selectedQuote); await sdk.repay(loanId, amount); await sdk.getLoanHistory(); ``` ### 3. Expiration Sessions expire after the configured validity period: ```typescript theme={null} const sdk = new BorrowSDK({ // ... sessionValiditySeconds: 259200 // 3 days (default) }); ``` ### 4. Renewal Create a new session by calling `setup()` again: ```typescript theme={null} // Check if session is expired if (Date.now() / 1000 > activeSession.validUntil) { await sdk.setup(); // Creates new session } ``` ## Checking Session Status ### Via User Status ```typescript theme={null} const { userStatus } = await sdk.setup(); console.log('Has session:', userStatus.hasActiveSession); console.log('Expires:', new Date(userStatus.sessionExpiry! * 1000)); ``` ### Manual Check ```typescript theme={null} function isSessionValid(session: ActiveSession): boolean { return Date.now() / 1000 < session.validUntil; } ``` ## Session Configuration ### Validity Duration ```typescript theme={null} const sdk = new BorrowSDK({ // ... sessionValiditySeconds: 86400 // 24 hours }); ``` | Duration | Seconds | Use Case | | -------- | ------- | -------------------- | | 1 hour | 3600 | High security | | 24 hours | 86400 | Daily usage | | 3 days | 259200 | Default, convenience | | 7 days | 604800 | Infrequent usage | ### Minimum Duration Sessions must be at least 60 seconds: ```typescript theme={null} sessionValiditySeconds: 60 // Minimum allowed ``` ## Session Flow Diagram ```mermaid theme={null} sequenceDiagram participant User participant SDK participant API participant Backend User->>SDK: setup() SDK->>User: prompt sign msg User-->>SDK: signature SDK->>API: POST /auth/session API->>Backend: create session Backend-->>API: session created API-->>SDK: session data SDK-->>User: session ready User->>SDK: executeBorrow(selectedQuote) SDK->>API: use session API-->>SDK: response SDK-->>User: workflow id ``` ## Clearing Sessions ### Clear Current Session ```typescript theme={null} sdk.clearSession(); ``` This clears: * Active session * Cached signatures * User status * Stops workflow tracking ### When to Clear * User logs out * User disconnects wallet * Security concern * Switching accounts ## Session Errors ### Expired Session ```typescript theme={null} try { await sdk.executeBorrow(selectedQuote); } catch (error) { if (error.message.includes('session expired')) { await sdk.setup(); // Refresh session await sdk.executeBorrow(selectedQuote); // Retry } } ``` ### No Active Session ```typescript theme={null} try { await sdk.executeBorrow(quote); } catch (error) { if (error instanceof SmartAccountError) { // Likely no active session await sdk.setup(); } } ``` ## Best Practices ### 1. Check Session Before Operations ```typescript theme={null} async function ensureSession(sdk: BorrowSDK) { if (!sdk.userStatus.hasActiveSession || Date.now() / 1000 > (sdk.userStatus.sessionExpiry || 0)) { await sdk.setup(); } } ``` ### 2. Handle Session Expiry Gracefully ```typescript theme={null} async function safeOperation( sdk: BorrowSDK, operation: () => Promise ): Promise { try { return await operation(); } catch (error) { if (isSessionError(error)) { await sdk.setup(); return await operation(); } throw error; } } ``` ### 3. Clear on Disconnect ```typescript theme={null} function onWalletDisconnect() { sdk.clearSession(); } ``` ### 4. Appropriate Validity Duration Choose based on your use case: * **Web app** - 24 hours to 3 days * **Mobile app** - 7 days * **Script/automation** - 1 hour # Smart wallets Source: https://developer.satsterminal.com/borrow/core-concepts/smart-wallets # Smart Wallets Smart wallets are the foundation of the SatsTerminal Borrow SDK. They provide secure, deterministic EVM accounts derived from Bitcoin wallet signatures. ## What is a Smart Wallet? A smart wallet is an ERC-4337 smart contract account that: * Is **derived deterministically** from a Bitcoin signature * Supports **gasless transactions** via account abstraction * Enables **session-based authorization** for secure operations * Provides **multi-chain support** with the same derivation ## How Derivation Works ```mermaid theme={null} flowchart LR BTC["Bitcoin Wallet (Private Key)"] SIG["Signature (Message)"] SA["Smart Account (EVM Address)"] BTC --> SIG --> SA ``` 1. User signs a deterministic message with their Bitcoin wallet 2. The signature is used to derive EVM private keys 3. A smart account address is computed from these keys 4. The same BTC wallet always produces the same smart account ### Signing Message Format ``` Sign this message to access your SatsTerminal Borrow Account. Loan Wallet #0 This signature will be used to derive your secure Smart Account keys. ``` The `Loan Wallet #` index allows multiple smart accounts per BTC wallet. ## Multi-Index Architecture Each user can have multiple smart wallets, indexed starting from 0: | Index | Purpose | | ----- | ---------------------------------------- | | 0 | Platform wallet (portfolio, withdrawals) | | 1+ | Loan wallets (one per loan) | ```typescript theme={null} // Platform wallet (index 0) - created during setup() await sdk.setup(); // Loan wallet (index 1) - created for first loan await sdk.executeBorrow(firstQuote); // Loan wallet (index 2) - created for second loan await sdk.executeBorrow(secondQuote); ``` ## Why Multiple Wallets? Using separate wallets per loan provides: 1. **Isolation** - Each loan's collateral is isolated 2. **Clarity** - Clear separation of funds 3. **Security** - Compromised loan doesn't affect others 4. **Tracking** - Easier transaction history per loan ## Signature Caching The SDK caches signatures to avoid repeated signing prompts: ```typescript theme={null} // First call - prompts for signature await sdk.setup(); // User signs message // Subsequent calls - uses cached signature await sdk.startNewLoan(); // No prompt if signature cached ``` Signatures are stored with namespaced keys: ``` @satsterminal/borrow/wallet_0_signature @satsterminal/borrow/wallet_1_signature ``` ### Clearing Cached Signatures ```typescript theme={null} // Clear all cached signatures sdk.clearSession(); ``` ## Smart Account Features ### Gasless Transactions Smart accounts enable gasless transactions via ERC-4337: * Users don't need ETH for gas * Gas is paid by the protocol * Transactions are bundled efficiently ### Session Keys Instead of signing every transaction, users authorize a session: ```typescript theme={null} const session = await sdk.setup(); console.log('Session valid until:', new Date(session.activeSession.validUntil * 1000)); ``` Sessions allow the SDK to execute transactions without additional signatures. ### Cross-Chain Support The same Bitcoin signature can be reused, but the smart account is still derived and checked in a chain-specific account context: ```typescript theme={null} // Same BTC wallet, different chains const sdkArb = new BorrowSDK({ apiKey, wallet }); const sdkBase = new BorrowSDK({ apiKey, wallet }); // Preload an explicit chain only when you need that chain's account/session context await sdkArb.setup({ chain: ChainType.ARBITRUM }); await sdkBase.setup(); ``` ## Wallet States ### Deployed vs Undeployed Smart accounts can exist in two states: | State | Description | | ---------- | ------------------------------------------ | | Undeployed | Address computed but contract not deployed | | Deployed | Contract deployed on-chain | ```typescript theme={null} const { userStatus } = await sdk.setup(); console.log('Is deployed:', userStatus.isDeployed); ``` Deployment happens automatically during the first transaction. ## Restoring Wallets If a user switches devices, wallets can be restored: ```typescript theme={null} // New device - no cached signature const sdk = new BorrowSDK(config); // Setup will prompt for signature again // Same signature = same smart account await sdk.setup(); ``` The deterministic derivation ensures the same BTC wallet always recovers the same smart accounts. ## Security Considerations ### Signature Security * Signatures are stored locally (browser localStorage or provided storage) * Never transmitted except for initial derivation * Can be cleared via `clearSession()` ### Smart Account Security * Controlled only by the derived keys * Session keys have limited scope and expiry * Multi-sig upgrades possible (future) ### Best Practices 1. **Clear sessions on logout** - Call `clearSession()` when user disconnects 2. **Use secure storage** - Provide encrypted storage in production 3. **Monitor session expiry** - Refresh sessions before they expire 4. **Validate addresses** - Always verify smart account addresses match expectations # Workflows Source: https://developer.satsterminal.com/borrow/core-concepts/workflows # Workflows Workflows are asynchronous, multi-step processes that track the progress of loan operations. ## What is a Workflow? A workflow represents a complex operation (like borrowing or repaying) that involves multiple steps and external events (like Bitcoin deposits). ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onStatusUpdate: (status) => { console.log(`Step ${status.step}: ${status.label}`); } }, 'borrow'); console.log('Workflow ID:', workflowId); ``` ## Workflow Types ### Borrow Workflow Tracks loan creation. These are the stages currently exposed by the SDK flow: | Stage | Description | | ------------------------------- | ------------------------------------------- | | `INITIALIZING` | Workflow starting | | `QUOTE_READY` | Bridge quote prepared | | `DEPOSIT_ADDRESS_READY` | Deposit address generated | | `AWAITING_DEPOSIT` | Waiting for BTC deposit | | `AWAITING_DEPOSIT_CONFIRMATION` | BTC detected; waiting for confirmations | | `DEPOSIT_CONFIRMED` | Deposit received | | `PREPARING_BORROW_DEPOSIT` | Preparing to supply collateral | | `COLLATERAL_DEPOSITED` | Collateral processed | | `PREPARING_LOAN` | Loan being prepared | | `LOAN_CONFIRMED` | Loan executed | | `PREPARING_DISBURSEMENT` | Preparing the destination payout | | `DISBURSEMENT_SUBMITTED` | Destination payout submitted | | `DISBURSEMENT_COMPLETED` | Destination payout confirmed | | `COMPLETED` | Workflow complete | | `FAILED` | Workflow failed | | `DISBURSEMENT_FAILED` | Loan is live, but destination payout failed | | `CANCELLED` | Workflow cancelled | | `REFUND_INITIATED` | Refund started after a bridge failure | | `REFUND_COMPLETED` | Deposited BTC refunded | `BorrowWorkflowStage` also contains compatibility stages used by other lending paths: `PREPARING_DEPOSIT`, `AUTO_TRANSFERRING_BTC`, `AWAITING_REDEEM`, `REDEEM_CONFIRMED`, `PREPARING_BORROW`, `PREPARING_REPAY`, `AWAITING_REPAY_CONFIRMATION`, `PREPARING_WITHDRAW`, and `AWAITING_WITHDRAW_CONFIRMATION`. A particular operation does not visit every stage. ### Borrow More Workflow Borrow More reuses existing collateral and therefore skips the Bitcoin bridge and collateral transaction. It still emits the shared position-preparation stage: `INITIALIZING → PREPARING_BORROW_DEPOSIT → PREPARING_LOAN → LOAN_CONFIRMED → PREPARING_DISBURSEMENT → DISBURSEMENT_SUBMITTED → DISBURSEMENT_COMPLETED → COMPLETED` ### Deposit More Workflow Deposit More bridges new Bitcoin collateral but does not borrow or disburse: `INITIALIZING → QUOTE_READY → DEPOSIT_ADDRESS_READY → AWAITING_DEPOSIT → AWAITING_DEPOSIT_CONFIRMATION → DEPOSIT_CONFIRMED → PREPARING_BORROW_DEPOSIT → COLLATERAL_DEPOSITED → COMPLETED` ### Repay Workflow Tracks the repayment process: | Stage | Description | | ----------------------------------- | --------------------- | | `INITIALIZING` | Starting repayment | | `TRANSFERRING_TO_KERNEL` | Moving funds | | `REPAYING_LOAN` | Executing repayment | | `WITHDRAWING_COLLATERAL` | Withdrawing BTC | | `TRANSFERRING_TO_PLATFORM_WALLET` | Preparing bridge | | `BRIDGE_INITIALIZING` | Bridge starting | | `BRIDGE_QUOTE_READY` | Bridge quote received | | `BRIDGE_SWAP_CREATED` | Swap created | | `BRIDGE_EXECUTING_APPROVAL` | Approving tokens | | `BRIDGE_APPROVAL_CONFIRMED` | Approval confirmed | | `BRIDGE_EXECUTING_INITIATE` | Initiating bridge | | `BRIDGE_INITIATE_CONFIRMED` | Bridge initiated | | `BRIDGE_AWAITING_BRIDGE_COMPLETION` | Waiting for bridge | | `BRIDGE_COMPLETED` | Bridge complete | | `COMPLETED` | Workflow complete | | `FAILED` | Workflow failed | | `CANCELLED` | Workflow cancelled | ### Withdrawal Workflows Bitcoin withdrawals use the bridge stages: `INITIALIZING → QUOTE_READY → SWAP_CREATED → EXECUTING_APPROVAL → APPROVAL_CONFIRMED → EXECUTING_INITIATE → INITIATE_CONFIRMED → AWAITING_BRIDGE_COMPLETION → BRIDGE_COMPLETED → COMPLETED` Direct EVM withdrawals use: `INITIALIZING → VALIDATING → CHECKING_BALANCE → EXECUTING_TRANSFER → COMPLETED` Both withdrawal flows can terminate at `FAILED` or `CANCELLED`. ## Status fields SDK status endpoints expose several related fields: | Field | Meaning | | --------------------- | ---------------------------------------------------------------- | | `status` / `stage` | Current workflow stage. They currently contain the same value. | | `transactionStatuses` | Public chronological stage history. | | `transactionState` | Compact current state used for tracking. | | `workflowState` | Deprecated compatibility alias for `transactionState`. | | `transactionDetails` | Public operation metadata; never the internal database document. | Do not depend on raw bridge quotes, database fields, or provider payloads. Those are intentionally not part of the public SDK response. Stage enums can grow as workflows become more detailed. Handle unknown stages gracefully and use `isComplete` and `isFailed` from `WorkflowStatus` for terminal UI behavior. ## Workflow Status Object ```typescript theme={null} interface WorkflowStatus { stage: string; // Current stage name step: number; // Step number (1-based) label: string; // Human-readable label description: string; // Detailed description depositAddress?: string; // BTC deposit address (if applicable) depositAmount?: number; // Required deposit in satoshis depositAmountBTC?: number; depositTxHash?: string; currentConfirmations?: number; requiredConfirmations?: number; error?: string; // Error message (if failed) isComplete: boolean; // Whether workflow is done isFailed: boolean; // Whether workflow failed rawData: any; // Public status endpoint response } ``` ## Tracking Workflows ### Tracking a Borrow Workflow After `executeBorrow()` returns a workflow ID, pass it to `trackWorkflow()`: ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onStatusUpdate: (status) => console.log(status.label), onComplete: () => console.log('Done!') }, 'borrow'); ``` ### Resuming Tracking Resume tracking a workflow that was interrupted: ```typescript theme={null} // Store workflow ID somewhere localStorage.setItem('pendingWorkflow', workflowId); // Later, resume tracking const workflowId = localStorage.getItem('pendingWorkflow'); if (workflowId) { await sdk.resumeLoan(workflowId, { onStatusUpdate: (status) => console.log(status), onComplete: () => { localStorage.removeItem('pendingWorkflow'); } }); } ``` ## Workflow Callbacks ### `onStatusUpdate` Called when the workflow stage changes: ```typescript theme={null} onStatusUpdate: (status: WorkflowStatus) => { console.log(`[${status.step}] ${status.label}`); console.log(` ${status.description}`); // Update UI progress updateProgressBar(status.step, getTotalSteps(status.stage)); } ``` ### `onDepositReady` Called when a deposit address is ready (borrow workflows): Treat this callback as time-sensitive in the UI: the deposit address is single-use and the quote stays locked for up to 6 hours. ```typescript theme={null} onDepositReady: (info: DepositInfo) => { console.log('Deposit Required:'); console.log(` Address: ${info.address}`); console.log(` Amount: ${info.amountBTC} BTC`); console.log(` Satoshis: ${info.amount}`); // Show QR code, send notification, etc. showDepositModal(info); } ``` ### `onComplete` Called when the workflow completes successfully: ```typescript theme={null} onComplete: (result: any) => { console.log('Workflow completed!'); console.log('Result:', result); // Navigate to success page, show notification, etc. router.push('/loan/success'); } ``` ### `onError` Called when the workflow fails: ```typescript theme={null} onError: (error: string) => { console.error('Workflow failed:', error); // Show error message, offer retry, etc. showErrorModal(error); } ``` ## Polling Configuration The SDK polls for workflow status at a configurable interval: ```typescript theme={null} const sdk = new BorrowSDK({ // ... workflowPollInterval: 2000 // Poll every 2 seconds (default) }); ``` | Interval | Use Case | | -------- | -------------------------------------- | | 1000ms | Responsive UI, more API calls | | 2000ms | Default, good balance | | 5000ms | Less frequent updates, fewer API calls | Minimum interval is 100ms. ## Stopping Tracking ### Stop All Tracking ```typescript theme={null} // Stops all active workflow trackers sdk.clearSession(); ``` ## Workflow State Diagram ### Borrow Workflow ```mermaid theme={null} %%{init: {"flowchart": {"curve": "basis"}} }%% flowchart TD INIT[INITIALIZING] ADDR[DEPOSIT_ADDRESS_READY] AWAIT[AWAITING_DEPOSIT] CONFIRM[DEPOSIT_CONFIRMED] COLL[COLLATERAL_DEPOSITED] PREP[PREPARING_LOAN] LC[LOAN_CONFIRMED] DONE[COMPLETED] FAIL[FAILED] INIT --> ADDR --> AWAIT AWAIT -->|deposit received| CONFIRM CONFIRM --> COLL --> PREP --> LC LC -->|success| DONE LC -->|error| FAIL AWAIT -- "timeout / retry" --> AWAIT ``` ## Getting Workflow Status Manually ```typescript theme={null} // Get current status without callbacks const status = await sdk.getStatus(workflowId); console.log('Stage:', status.stage); console.log('Complete:', status.isComplete); ``` ## Handling Pending Workflows Check for and resume pending workflows on app load: ```typescript theme={null} async function checkPendingWorkflows(sdk: BorrowSDK) { const pending = await sdk.getPendingLoans(); for (const loan of pending) { const workflowId = loan.borrowTransaction?.workflowId; if (workflowId) { console.log(`Found pending loan: ${workflowId}`); await sdk.resumeLoan(workflowId, { onStatusUpdate: (s) => console.log(s.label), onComplete: () => console.log('Resumed loan completed'), onDepositReady: (info) => { console.log(`Still waiting for deposit to ${info.address}`); } }); } } } ``` ## Best Practices ### 1. Always Handle Errors ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onError: (error) => { // Log for debugging console.error('Workflow error:', error); // Show user-friendly message showNotification('Loan failed. Please try again.'); // Track for analytics analytics.track('loan_failed', { error }); } }, 'borrow'); ``` ### 2. Persist Workflow IDs ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote); // Persist for resumption await saveWorkflowId(workflowId); // On completion, clean up onComplete: async () => { await clearWorkflowId(workflowId); } ``` ### 3. Show Progress Indicator ```typescript theme={null} const stages = ['INITIALIZING', 'AWAITING_DEPOSIT', 'PROCESSING', 'COMPLETED']; onStatusUpdate: (status) => { const index = stages.indexOf(status.stage); const progress = (index / stages.length) * 100; updateProgressBar(progress); } ``` ### 4. Handle Page Refresh ```typescript theme={null} // On page load useEffect(() => { const pendingId = localStorage.getItem('workflowId'); if (pendingId) { sdk.resumeLoan(pendingId, callbacks); } }, []); ``` # Complete loan flow Source: https://developer.satsterminal.com/borrow/examples/complete-loan-flow # Complete Loan Flow This example demonstrates the entire loan lifecycle from setup to repayment. ## Overview ```mermaid theme={null} flowchart LR SETUP["Setup"] QUOTE["Quote"] BORROW["Borrow"] MONITOR["Monitor"] REPAY["Repay"] SETUP --> QUOTE --> BORROW --> MONITOR --> REPAY ``` ## Complete Example ```typescript theme={null} import { BorrowSDK, ChainType, Quote, UserTransaction, WorkflowStatus, DepositInfo, LoanCollateralInfo } from '@satsterminal-sdk/borrow'; // Configuration const CONFIG = { apiKey: process.env.SATSTERMINAL_API_KEY!, btcAddress: process.env.BTC_ADDRESS! }; // Loan parameters const LOAN_PARAMS = { collateralBTC: 0.1, loanAmountUSD: 5000 }; class LoanManager { private sdk: BorrowSDK; private activeLoanId: string | null = null; private workflowId: string | null = null; constructor(signMessage: (msg: string) => Promise) { this.sdk = new BorrowSDK({ ...CONFIG, wallet: { address: CONFIG.btcAddress, signMessage } }); } // ======================================== // PHASE 1: SETUP // ======================================== async setup(): Promise { console.log('='.repeat(50)); console.log('PHASE 1: SETUP'); console.log('='.repeat(50)); const { platformWallet, userStatus, activeSession, transactions } = await this.sdk.setup(); console.log('\nSetup Complete:'); console.log(` BTC Address: ${userStatus.btcAddress}`); console.log(` Smart Account: ${platformWallet.address}`); console.log(` Is Deployed: ${userStatus.isDeployed}`); console.log(` Session Expires: ${new Date(activeSession.validUntil * 1000).toLocaleString()}`); console.log(` Existing Transactions: ${transactions.length}`); } // ======================================== // PHASE 2: GET QUOTES // ======================================== async getQuotes(): Promise { console.log('\n' + '='.repeat(50)); console.log('PHASE 2: GET QUOTES'); console.log('='.repeat(50)); const quotes = await this.sdk.getQuotes({ collateralAmount: LOAN_PARAMS.collateralBTC.toString(), loanAmount: LOAN_PARAMS.loanAmountUSD.toString() }); console.log(`\nFound ${quotes.length} quotes:`); quotes.forEach((q, i) => { console.log(`\n ${i + 1}. ${q.protocol.toUpperCase()}`); console.log(` Chain: ${q.chain}`); console.log(` Collateral: ${q.collateralAmount} BTC`); console.log(` Loan Amount: $${q.loanAmount}`); console.log(` Variable APY: ${q.borrowApy.variable}%`); console.log(` Stable APY: ${q.borrowApy.stable}%`); }); return quotes; } selectBestQuote(quotes: Quote[]): Quote { // Select quote with lowest variable APY const bestQuote = quotes.reduce((best, current) => { const bestApy = parseFloat(best.borrowApy.variable); const currentApy = parseFloat(current.borrowApy.variable); return currentApy < bestApy ? current : best; }); console.log(`\nSelected: ${bestQuote.protocol} with ${bestQuote.borrowApy.variable}% APY`); return bestQuote; } // Fetch the fee breakdown for the selected quote. Bridge fees and // borrow-side fees (platform fee, disbursement bps, net loan amount, // any campaign waiver) are returned separately from the quote. async getQuoteFees(quote: Quote) { const fees = await this.sdk.getQuoteFees({ collateralAmount: LOAN_PARAMS.collateralBTC.toString(), loanAmount: LOAN_PARAMS.loanAmountUSD.toString(), fromChain: ChainType.BITCOIN, fromAssetSymbol: 'BTC', toChain: quote.chain, toAssetSymbol: 'WBTC', loanChain: quote.chain, loanAssetSymbol: 'USDC', }); console.log(`\nBridge fee USD: ${fees.bridgeFees.totalBridgeFeeUSD}`); console.log(`Platform fee: ${fees.borrowFees.platformFee}`); console.log(`Net loan to user: ${fees.borrowFees.netLoanAmount}`); if (fees.borrowFees.feeSource === 'campaign') { console.log(`Campaign discount: ${fees.borrowFees.campaignSlug}`); } return fees; } // ======================================== // PHASE 3: EXECUTE BORROW // ======================================== async executeBorrow(selectedQuote: Quote): Promise { console.log('\n' + '='.repeat(50)); console.log('PHASE 3: EXECUTE BORROW'); console.log('='.repeat(50)); return new Promise(async (resolve, reject) => { try { const workflowId = await this.sdk.executeBorrow(selectedQuote); this.workflowId = workflowId; console.log(`\nWorkflow Started: ${this.workflowId}`); await this.sdk.trackWorkflow(workflowId, { onStatusUpdate: (status: WorkflowStatus) => { this.handleStatusUpdate(status); }, onDepositReady: (info: DepositInfo) => { this.handleDepositReady(info); }, onComplete: (result: any) => { this.handleBorrowComplete(result); resolve(); }, onError: (error: string) => { console.error('\n❌ Borrow Error:', error); reject(new Error(error)); } }, 'borrow'); } catch (error) { reject(error); } }); } private handleStatusUpdate(status: WorkflowStatus): void { const icon = status.isComplete ? '✅' : status.isFailed ? '❌' : '⏳'; console.log(`\n${icon} [Step ${status.step}] ${status.label}`); console.log(` ${status.description}`); if (status.stage === 'COLLATERAL_DEPOSITED') { console.log(' 💰 Collateral received and processing...'); } } private handleDepositReady(info: DepositInfo): void { console.log('\n' + '─'.repeat(50)); console.log('📥 DEPOSIT REQUIRED'); console.log('─'.repeat(50)); console.log(` Amount: ${info.amountBTC} BTC (${info.amount} sats)`); console.log(` Address: ${info.address}`); console.log('─'.repeat(50)); console.log('\n⏳ Waiting for deposit confirmation...'); } private handleBorrowComplete(result: any): void { console.log('\n' + '─'.repeat(50)); console.log('✅ BORROW COMPLETE'); console.log('─'.repeat(50)); console.log(` Workflow ID: ${this.workflowId}`); console.log(' Funds are now in your smart account.'); } // ======================================== // PHASE 4: MONITOR LOAN // ======================================== async monitorLoan(): Promise { console.log('\n' + '='.repeat(50)); console.log('PHASE 4: MONITOR LOAN'); console.log('='.repeat(50)); // Get active loans const history = await this.sdk.getLoanHistory({ status: 'active' }); if (history.transactions.length === 0) { console.log('\nNo active loans found.'); return; } const loan = history.transactions[0]; this.activeLoanId = loan.id; console.log(`\nActive Loan: ${loan.id}`); console.log(` Type: ${loan.type}`); console.log(` Amount: ${loan.amount} ${loan.currency}`); console.log(` Status: ${loan.status}`); console.log(` Created: ${new Date(loan.timestamp).toLocaleString()}`); // Get collateral info const collateralInfo = await this.sdk.getLoanCollateralInfo(loan.id); if (collateralInfo) { console.log('\nCollateral Information:'); console.log(` Total Collateral: ${collateralInfo.totalCollateral} BTC`); console.log(` Available Collateral: ${collateralInfo.availableCollateral} BTC`); console.log(` Max Withdrawable: ${collateralInfo.maxWithdrawable} BTC`); console.log(` Total Debt: $${collateralInfo.totalDebt}`); console.log(` Remaining Debt: $${collateralInfo.remainingDebt}`); // Calculate health const health = parseFloat(collateralInfo.availableCollateral) / parseFloat(collateralInfo.totalDebt); console.log(` Health Ratio: ${health.toFixed(4)}`); } // Check wallet positions const positions = await this.sdk.getWalletPositions(); console.log('\nWallet Positions:'); positions.data.forEach(p => { const symbol = p.attributes.fungible_info?.symbol || 'Unknown'; const amount = p.attributes.quantity.float; const value = p.attributes.value || 0; console.log(` ${symbol}: ${amount.toFixed(4)} ($${value.toFixed(2)})`); }); } // ======================================== // PHASE 5: REPAY LOAN // ======================================== async repayLoan(btcWithdrawAddress: string): Promise { console.log('\n' + '='.repeat(50)); console.log('PHASE 5: REPAY LOAN'); console.log('='.repeat(50)); if (!this.activeLoanId) { // Get active loan const history = await this.sdk.getLoanHistory({ status: 'active' }); if (history.transactions.length === 0) { console.log('\nNo active loans to repay.'); return; } this.activeLoanId = history.transactions[0].id; } // Get collateral info for repayment const collateralInfo = await this.sdk.getLoanCollateralInfo(this.activeLoanId); if (!collateralInfo) { console.error('\nCould not get collateral info.'); return; } console.log(`\nRepaying loan ${this.activeLoanId}`); console.log(` Debt: $${collateralInfo.remainingDebt}`); console.log(` Collateral to withdraw: ${collateralInfo.maxWithdrawable} BTC`); console.log(` Withdraw to: ${btcWithdrawAddress}`); return new Promise(async (resolve, reject) => { try { const txId = await this.sdk.repay( this.activeLoanId!, collateralInfo.remainingDebt, // Full repayment { collateralToWithdraw: collateralInfo.maxWithdrawable, userBtcWithdrawAddress: btcWithdrawAddress, trackWorkflow: true, callbacks: { onStatusUpdate: (status: WorkflowStatus) => { console.log(`\n⏳ [${status.step}] ${status.label}`); if (status.stage.includes('BRIDGE')) { console.log(' 🌉 Bridge in progress...'); } }, onComplete: () => { console.log('\n' + '─'.repeat(50)); console.log('✅ REPAYMENT COMPLETE'); console.log('─'.repeat(50)); console.log(' Loan fully repaid.'); console.log(` Collateral withdrawn to ${btcWithdrawAddress}`); resolve(); }, onError: (error: string) => { console.error('\n❌ Repayment Error:', error); reject(new Error(error)); } } } ); console.log(`\nRepayment Transaction: ${txId}`); } catch (error) { reject(error); } }); } // ======================================== // CLEANUP // ======================================== cleanup(): void { console.log('\n' + '='.repeat(50)); console.log('CLEANUP'); console.log('='.repeat(50)); this.sdk.clearSession(); console.log('\nSession cleared.'); } } // ======================================== // MAIN EXECUTION // ======================================== async function main() { console.log('\n'); console.log('╔══════════════════════════════════════════════════╗'); console.log('║ SATSTERMINAL BORROW - COMPLETE LOAN FLOW ║'); console.log('╚══════════════════════════════════════════════════╝'); console.log('\n'); // Initialize with your signing function const signMessage = async (message: string): Promise => { // Replace with your actual wallet signing implementation return await yourWallet.signMessage(message); }; const manager = new LoanManager(signMessage); try { // Phase 1: Setup await manager.setup(); // Phase 2: Get Quotes const quotes = await manager.getQuotes(); const selectedQuote = manager.selectBestQuote(quotes); // Fetch the fee breakdown for the selected quote. await manager.getQuoteFees(selectedQuote); // Phase 3: Execute Borrow await manager.executeBorrow(selectedQuote); // Phase 4: Monitor (wait a bit for loan to be active) console.log('\n⏳ Waiting for loan to become active...'); await new Promise(r => setTimeout(r, 5000)); await manager.monitorLoan(); // Phase 5: Repay (optional - uncomment to repay) // const btcAddress = 'bc1q...'; // await manager.repayLoan(btcAddress); console.log('\n'); console.log('╔══════════════════════════════════════════════════╗'); console.log('║ LOAN FLOW COMPLETE ║'); console.log('╚══════════════════════════════════════════════════╝'); } catch (error) { console.error('\n❌ Error:', error); } finally { manager.cleanup(); } } main().catch(console.error); ``` ## Flow Diagram ```mermaid theme={null} flowchart TD SETUP["SETUP
1. Create SDK instance
2. Call sdk.setup()
3. User signs message
4. Smart account derived
5. Session created"] QUOTES["GET QUOTES
1. Call sdk.getQuotes()
2. Compare APYs
3. Select best quote"] BORROW["EXECUTE BORROW
1. Call sdk.executeBorrow()
2. Track workflow
3. User deposits BTC
4. Deposit confirmed
5. Loan executed"] MONITOR["MONITOR LOAN
1. Check loan history
2. Get collateral info
3. Check wallet positions
4. Monitor health factor"] REPAY["REPAY LOAN
1. Call sdk.repay()
2. Specify collateral to withdraw
3. Wait for repayment + bridge
4. BTC sent to withdrawal address"] CLEAN["CLEANUP
1. Clear session
2. Reset state"] SETUP --> QUOTES --> BORROW --> MONITOR --> REPAY --> CLEAN BORROW -. "onDepositReady (manual or auto)" .-> MONITOR ``` ## Key Takeaways 1. **Use `setup()` for account preload** - Borrow-only flows can let `executeBorrow()` prepare the smart account and session 2. **Handle all callbacks** - Status updates, deposit ready, complete, and error 3. **Monitor loan health** - Check collateral info periodically 4. **Provide BTC address for repayment** - Required for collateral withdrawal 5. **Clear session when done** - Cleanup resources properly # Nodejs usage Source: https://developer.satsterminal.com/borrow/examples/nodejs-usage # Node.js Usage This guide shows how to use the SatsTerminal Borrow SDK in a Node.js environment. ## Setup ### Installation ```bash theme={null} npm install @satsterminal-sdk/borrow ``` ### Environment Variables ```bash theme={null} # .env SATSTERMINAL_API_KEY=your-api-key SATSTERMINAL_BASE_URL=https://api.satsterminal.com BTC_ADDRESS=bc1q... BTC_PRIVATE_KEY=... # For signing (use secure key management in production) ``` ## Basic Configuration ```typescript theme={null} import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; import * as bitcoin from 'bitcoinjs-lib'; import * as ecc from 'tiny-secp256k1'; import { ECPairFactory } from 'ecpair'; const ECPair = ECPairFactory(ecc); // Load environment variables const config = { apiKey: process.env.SATSTERMINAL_API_KEY!, btcAddress: process.env.BTC_ADDRESS!, btcPrivateKey: process.env.BTC_PRIVATE_KEY! }; // Create key pair for signing const keyPair = ECPair.fromWIF(config.btcPrivateKey); // Create SDK instance const sdk = new BorrowSDK({ apiKey: config.apiKey, wallet: { address: config.btcAddress, signMessage: async (message: string) => { // Sign message with Bitcoin private key const signature = keyPair.sign( bitcoin.crypto.sha256(Buffer.from(message)) ); return signature.toString('base64'); } }, // Use memory storage in Node.js storage: { _data: new Map(), getItem(key: string) { return this._data.get(key) || null; }, setItem(key: string, value: string) { this._data.set(key, value); }, removeItem(key: string) { this._data.delete(key); }, clear() { this._data.clear(); } } }); ``` ## CLI Application ```typescript theme={null} // cli.ts import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; import { Command } from 'commander'; const program = new Command(); let sdk: BorrowSDK; async function initSDK() { sdk = new BorrowSDK({ apiKey: process.env.API_KEY!, wallet: { address: process.env.BTC_ADDRESS!, signMessage: async (msg) => signWithWallet(msg) } }); await sdk.setup(); console.log('SDK initialized'); } program .name('borrow-cli') .description('SatsTerminal Borrow CLI') .version('1.0.0'); program .command('status') .description('Show current status') .action(async () => { await initSDK(); console.log('User Status:', sdk.userStatus); }); program .command('quote') .description('Get loan quotes') .option('-c, --collateral ', 'Collateral amount in BTC', '0.1') .option('-l, --loan ', 'Loan amount in USD', '5000') .action(async (options) => { await initSDK(); const quotes = await sdk.getQuotes({ collateralAmount: options.collateral, loanAmount: options.loan }); console.log('\nAvailable Quotes:'); quotes.forEach((q, i) => { console.log(`${i + 1}. ${q.protocol} - APY: ${q.borrowApy.variable}%`); }); }); program .command('borrow') .description('Get a loan') .option('-c, --collateral ', 'Collateral amount in BTC', '0.1') .option('-l, --loan ', 'Loan amount in USD', '5000') .action(async (options) => { await initSDK(); console.log('Starting loan...'); const quotes = await sdk.getQuotes({ collateralAmount: options.collateral, loanAmount: options.loan, }); const selectedQuote = quotes[0]; const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onStatusUpdate: (status) => { console.log(`[${status.step}] ${status.label}`); }, onDepositReady: (info) => { console.log('\n=== DEPOSIT REQUIRED ==='); console.log(`Amount: ${info.amountBTC} BTC`); console.log(`Address: ${info.address}`); console.log('========================\n'); }, onComplete: () => { console.log('\nLoan complete!'); process.exit(0); }, onError: (error) => { console.error('\nError:', error); process.exit(1); } }, 'borrow'); }); program .command('history') .description('Show loan history') .option('-s, --status ', 'Filter by status (active|pending|all)', 'all') .action(async (options) => { await initSDK(); const history = await sdk.getLoanHistory({ status: options.status as 'active' | 'pending' | 'all' }); console.log(`\nLoan History (${history.pagination.totalTransactions} total):\n`); history.transactions.forEach((tx) => { console.log(`ID: ${tx.id}`); console.log(` Type: ${tx.type}`); console.log(` Amount: ${tx.amount} ${tx.currency}`); console.log(` Status: ${tx.status}`); console.log(` Date: ${new Date(tx.timestamp).toLocaleString()}`); console.log(''); }); }); program .command('positions') .description('Show wallet positions') .action(async () => { await initSDK(); const positions = await sdk.getWalletPositions(); console.log('\nWallet Positions:\n'); let totalValue = 0; positions.data.forEach((p) => { const symbol = p.attributes.fungible_info?.symbol || 'Unknown'; const amount = p.attributes.quantity.float; const value = p.attributes.value || 0; totalValue += value; console.log(`${symbol}: ${amount.toFixed(4)} ($${value.toFixed(2)})`); }); console.log(`\nTotal Value: $${totalValue.toFixed(2)}`); }); program .command('repay ') .description('Repay a loan') .option('-w, --withdraw ', 'Withdraw collateral') .option('-a, --address ', 'BTC address for withdrawal') .action(async (loanId, amount, options) => { await initSDK(); console.log(`Repaying ${amount} USD...`); await sdk.repay(loanId, amount, { collateralToWithdraw: options.withdraw, userBtcWithdrawAddress: options.address, trackWorkflow: true, callbacks: { onStatusUpdate: (status) => { console.log(`[${status.step}] ${status.label}`); }, onComplete: () => { console.log('\nRepayment complete!'); process.exit(0); }, onError: (error) => { console.error('\nError:', error); process.exit(1); } } }); }); program.parse(); ``` ## Automation Scripts ### Automated Loan Monitoring ```typescript theme={null} // monitor.ts import { BorrowSDK, ChainType, UserTransaction } from '@satsterminal-sdk/borrow'; async function monitorLoans(sdk: BorrowSDK) { console.log('Starting loan monitor...'); setInterval(async () => { try { const history = await sdk.getLoanHistory({ status: 'active' }); for (const loan of history.transactions) { const collateral = await sdk.getLoanCollateralInfo(loan.id); if (collateral) { const healthRatio = parseFloat(collateral.availableCollateral) / parseFloat(collateral.totalDebt); console.log(`Loan ${loan.id.slice(0, 8)}... Health: ${healthRatio.toFixed(2)}`); // Alert if health is low if (healthRatio < 1.2) { console.warn(`⚠️ LOW HEALTH: Loan ${loan.id} health is ${healthRatio.toFixed(2)}`); // Send notification (email, Slack, etc.) await sendAlert({ type: 'low_health', loanId: loan.id, health: healthRatio }); } } } } catch (error) { console.error('Monitor error:', error); } }, 60000); // Check every minute } ``` ### Batch Operations ```typescript theme={null} // batch.ts import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; async function batchRepay( sdk: BorrowSDK, loans: Array<{ id: string; amount: string }> ) { console.log(`Processing ${loans.length} repayments...`); const results: Array<{ id: string; success: boolean; error?: string }> = []; for (const loan of loans) { try { console.log(`Repaying loan ${loan.id}...`); await sdk.repay(loan.id, loan.amount, { trackWorkflow: true, callbacks: { onComplete: () => { console.log(`✓ Loan ${loan.id} repaid`); } } }); results.push({ id: loan.id, success: true }); } catch (error) { console.error(`✗ Loan ${loan.id} failed:`, error); results.push({ id: loan.id, success: false, error: error instanceof Error ? error.message : 'Unknown error' }); } // Rate limiting await new Promise(r => setTimeout(r, 2000)); } // Summary const successful = results.filter(r => r.success).length; console.log(`\nCompleted: ${successful}/${loans.length} successful`); return results; } ``` ### Scheduled Tasks ```typescript theme={null} // scheduler.ts import cron from 'node-cron'; import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; async function setupScheduler(sdk: BorrowSDK) { // Check positions every hour cron.schedule('0 * * * *', async () => { console.log('Running hourly position check...'); const positions = await sdk.getWalletPositions(); // Process positions }); // Check loan health every 15 minutes cron.schedule('*/15 * * * *', async () => { console.log('Running health check...'); const history = await sdk.getLoanHistory({ status: 'active' }); for (const loan of history.transactions) { const info = await sdk.getLoanCollateralInfo(loan.id); // Check health and alert if needed } }); // Daily summary at 9 AM cron.schedule('0 9 * * *', async () => { console.log('Generating daily summary...'); const portfolio = await sdk.getWalletPortfolio(); const history = await sdk.getLoanHistory({ status: 'all' }); // Generate and send report }); console.log('Scheduler started'); } ``` ## API Server ```typescript theme={null} // server.ts import express from 'express'; import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; const app = express(); app.use(express.json()); // SDK instances per user (in production, use proper session management) const sdkInstances = new Map(); function getSDK(userId: string): BorrowSDK { if (!sdkInstances.has(userId)) { throw new Error('SDK not initialized for user'); } return sdkInstances.get(userId)!; } // Initialize SDK for user app.post('/api/init', async (req, res) => { try { const { userId, btcAddress, signature } = req.body; const sdk = new BorrowSDK({ apiKey: process.env.API_KEY!, wallet: { address: btcAddress, signMessage: async () => signature // Pre-signed } }); await sdk.setup(); sdkInstances.set(userId, sdk); res.json({ success: true, userStatus: sdk.userStatus }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get quotes app.post('/api/quotes', async (req, res) => { try { const sdk = getSDK(req.body.userId); const quotes = await sdk.getQuotes(req.body.params); res.json({ quotes }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get loan history app.get('/api/loans/:userId', async (req, res) => { try { const sdk = getSDK(req.params.userId); const history = await sdk.getLoanHistory({ status: (req.query.status as any) || 'all' }); res.json(history); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get positions app.get('/api/positions/:userId', async (req, res) => { try { const sdk = getSDK(req.params.userId); const positions = await sdk.getWalletPositions(); res.json(positions); } catch (error) { res.status(500).json({ error: error.message }); } }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', instances: sdkInstances.size }); }); app.listen(3000, () => { console.log('API server running on port 3000'); }); ``` ## Best Practices ### Error Handling ```typescript theme={null} import { BorrowSDKError, ApiError } from '@satsterminal-sdk/borrow'; async function safeOperation( operation: () => Promise, retries = 3 ): Promise { let lastError: Error | undefined; for (let i = 0; i < retries; i++) { try { return await operation(); } catch (error) { lastError = error as Error; if (error instanceof ApiError) { if (error.statusCode === 429) { // Rate limited - wait and retry await new Promise(r => setTimeout(r, 5000 * (i + 1))); continue; } if (error.statusCode && error.statusCode >= 500) { // Server error - retry await new Promise(r => setTimeout(r, 1000 * (i + 1))); continue; } } // Non-retryable error throw error; } } throw lastError; } ``` ### Logging ```typescript theme={null} import winston from 'winston'; const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }) ] }); const sdk = new BorrowSDK({ // ...config logger: { debug: (msg) => logger.debug(msg), info: (msg) => logger.info(msg), warn: (msg) => logger.warn(msg), error: (msg) => logger.error(msg) } }); ``` ### Graceful Shutdown ```typescript theme={null} let sdk: BorrowSDK; process.on('SIGTERM', async () => { console.log('Shutting down...'); if (sdk) { sdk.clearSession(); } process.exit(0); }); process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); if (sdk) { sdk.clearSession(); } process.exit(1); }); ``` # React integration Source: https://developer.satsterminal.com/borrow/examples/react-integration # React Integration This guide shows how to integrate the SatsTerminal Borrow SDK into a React application. For a complete working application, see the official [Borrow SDK Example](https://github.com/Sats-Terminal/borrow-sdk-example/tree/main). It demonstrates the SDK through individually composed Borrow UI registry components rather than the all-in-one `BorrowApp`. ## Setup ### Install Dependencies ```bash theme={null} npm install @satsterminal-sdk/borrow ``` ### Create SDK Context ```typescript theme={null} // src/contexts/BorrowContext.tsx import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react'; import { BorrowSDK, ChainType, UserStatus, BorrowSDKConfig } from '@satsterminal-sdk/borrow'; interface BorrowContextType { sdk: BorrowSDK | null; userStatus: UserStatus | null; isInitialized: boolean; isLoading: boolean; error: string | null; initialize: (walletAddress: string, signMessage: (msg: string) => Promise) => Promise; disconnect: () => void; } const BorrowContext = createContext(undefined); interface BorrowProviderProps { children: ReactNode; apiKey: string; chain?: ChainType; } export function BorrowProvider({ children, apiKey, baseUrl, chain = ChainType.ARBITRUM }: BorrowProviderProps) { const [sdk, setSdk] = useState(null); const [userStatus, setUserStatus] = useState(null); const [isInitialized, setIsInitialized] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const initialize = useCallback(async ( walletAddress: string, signMessage: (msg: string) => Promise ) => { setIsLoading(true); setError(null); try { const config: BorrowSDKConfig = { apiKey, wallet: { address: walletAddress, signMessage } }; const borrowSdk = new BorrowSDK(config); const { userStatus: status } = await borrowSdk.setup(); setSdk(borrowSdk); setUserStatus(status); setIsInitialized(true); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to initialize'); throw err; } finally { setIsLoading(false); } }, [apiKey, baseUrl, chain]); const disconnect = useCallback(() => { if (sdk) { sdk.clearSession(); } setSdk(null); setUserStatus(null); setIsInitialized(false); }, [sdk]); return ( {children} ); } export function useBorrowContext() { const context = useContext(BorrowContext); if (!context) { throw new Error('useBorrowContext must be used within BorrowProvider'); } return context; } ``` ### Wrap Your App ```typescript theme={null} // src/App.tsx import { BorrowProvider } from './contexts/BorrowContext'; function App() { return ( ); } ``` ## Utility Imports Import the `Units` utility for type-safe BTC/satoshi conversions: ```typescript theme={null} import { Units, type Satoshis, type BTC } from '@satsterminal-sdk/borrow'; // Convert and display amounts function formatCollateral(sats: number): string { const btc = Units.normalizeToBtc(sats); return `${Units.formatBtc(btc, 4)} BTC`; } // Convert user input to satoshis for API calls function parseUserBTC(input: string): Satoshis { return Units.btcToSats(input); } ``` ## Custom Hooks ### useLoan Hook ```typescript theme={null} // src/hooks/useLoan.ts import { useState, useCallback } from 'react'; import { useBorrowContext } from '../contexts/BorrowContext'; import { WorkflowStatus, DepositInfo, Quote } from '@satsterminal-sdk/borrow'; interface UseLoanOptions { collateralBTC: number; loanAmountUSD: number; } interface UseLoanReturn { isLoading: boolean; status: WorkflowStatus | null; depositInfo: DepositInfo | null; quote: Quote | null; error: string | null; isComplete: boolean; startLoan: () => Promise; } export function useLoan(options: UseLoanOptions): UseLoanReturn { const { sdk } = useBorrowContext(); const [isLoading, setIsLoading] = useState(false); const [status, setStatus] = useState(null); const [depositInfo, setDepositInfo] = useState(null); const [quote, setQuote] = useState(null); const [error, setError] = useState(null); const [isComplete, setIsComplete] = useState(false); const startLoan = useCallback(async () => { if (!sdk) { setError('SDK not initialized'); return; } setIsLoading(true); setError(null); setIsComplete(false); setStatus(null); setDepositInfo(null); try { const quotes = await sdk.getQuotes({ collateralAmount: options.collateralBTC.toString(), loanAmount: options.loanAmountUSD.toString(), }); const selectedQuote = quotes[0]; setQuote(selectedQuote); const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onStatusUpdate: setStatus, onDepositReady: setDepositInfo, onComplete: () => { setIsComplete(true); setIsLoading(false); }, onError: (err) => { setError(err); setIsLoading(false); } }, 'borrow'); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to start loan'); setIsLoading(false); } }, [sdk, options]); return { isLoading, status, depositInfo, quote, error, isComplete, startLoan }; } ``` ### usePositions Hook ```typescript theme={null} // src/hooks/usePositions.ts import { useState, useEffect, useCallback } from 'react'; import { useBorrowContext } from '../contexts/BorrowContext'; import { WalletPosition } from '@satsterminal-sdk/borrow'; export function usePositions(refreshInterval = 30000) { const { sdk, isInitialized } = useBorrowContext(); const [positions, setPositions] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const fetchPositions = useCallback(async () => { if (!sdk || !isInitialized) return; setIsLoading(true); try { const response = await sdk.getWalletPositions({ filterTrash: 'only_non_trash' }); setPositions(response.data); setError(null); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch positions'); } finally { setIsLoading(false); } }, [sdk, isInitialized]); useEffect(() => { fetchPositions(); const interval = setInterval(fetchPositions, refreshInterval); return () => clearInterval(interval); }, [fetchPositions, refreshInterval]); return { positions, isLoading, error, refresh: fetchPositions }; } ``` ### useLoanHistory Hook ```typescript theme={null} // src/hooks/useLoanHistory.ts import { useState, useEffect, useCallback } from 'react'; import { useBorrowContext } from '../contexts/BorrowContext'; import { UserTransaction } from '@satsterminal-sdk/borrow'; export function useLoanHistory(status: 'active' | 'pending' | 'all' = 'all') { const { sdk, isInitialized } = useBorrowContext(); const [loans, setLoans] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const fetchLoans = useCallback(async (pageNum: number) => { if (!sdk || !isInitialized) return; setIsLoading(true); try { const response = await sdk.getLoanHistory({ page: pageNum, limit: 10, status }); if (pageNum === 1) { setLoans(response.transactions); } else { setLoans(prev => [...prev, ...response.transactions]); } setHasMore(response.pagination.hasNext); setError(null); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch loans'); } finally { setIsLoading(false); } }, [sdk, isInitialized, status]); useEffect(() => { setPage(1); fetchLoans(1); }, [fetchLoans]); const loadMore = useCallback(() => { if (!isLoading && hasMore) { const nextPage = page + 1; setPage(nextPage); fetchLoans(nextPage); } }, [isLoading, hasMore, page, fetchLoans]); return { loans, isLoading, error, hasMore, loadMore }; } ``` ## Components ### LoanForm Component ```typescript theme={null} // src/components/LoanForm.tsx import React, { useState } from 'react'; import { useLoan } from '../hooks/useLoan'; export function LoanForm() { const [collateralBTC, setCollateralBTC] = useState(0.1); const [loanAmountUSD, setLoanAmountUSD] = useState(5000); const { isLoading, status, depositInfo, quote, error, isComplete, startLoan } = useLoan({ collateralBTC, loanAmountUSD }); return (

Get a Loan

{!isLoading && !status && (
{ e.preventDefault(); startLoan(); }}>
setCollateralBTC(parseFloat(e.target.value))} />
setLoanAmountUSD(parseFloat(e.target.value))} />
)} {status && (

Status: {status.label}

{status.description}

)} {depositInfo && !isComplete && (

Deposit Required

Amount: {depositInfo.amountBTC} BTC

{depositInfo.address}
)} {quote && (

Quote Details

Protocol: {quote.protocol}

APY: {quote.borrowApy.variable}%

)} {isComplete && (

Loan Complete!

Your funds are now available in your smart account.

)} {error && (

Error: {error}

)}
); } ``` ### PositionsList Component ```typescript theme={null} // src/components/PositionsList.tsx import React from 'react'; import { usePositions } from '../hooks/usePositions'; export function PositionsList() { const { positions, isLoading, error, refresh } = usePositions(); if (isLoading && positions.length === 0) { return
Loading positions...
; } if (error) { return
Error: {error}
; } const totalValue = positions.reduce( (sum, p) => sum + (p.attributes.value || 0), 0 ); return (

Wallet Positions

Total Value: ${totalValue.toFixed(2)}
{positions.map((position) => ( ))}
Token Amount Value 24h
{position.attributes.fungible_info?.symbol || 'Unknown'} {position.attributes.quantity.float.toFixed(4)} ${(position.attributes.value || 0).toFixed(2)} = 0 ? 'positive' : 'negative' }> {position.attributes.changes?.percent_1d?.toFixed(2) || 0}%
); } ``` ### ConnectWallet Component ```typescript theme={null} // src/components/ConnectWallet.tsx import React from 'react'; import { useBorrowContext } from '../contexts/BorrowContext'; export function ConnectWallet() { const { isInitialized, isLoading, error, initialize, disconnect, userStatus } = useBorrowContext(); const handleConnect = async () => { // Example with a Bitcoin wallet library // Replace with your actual wallet integration const wallet = await connectBitcoinWallet(); await initialize( wallet.address, (message) => wallet.signMessage(message) ); }; if (isInitialized) { return (

Connected: {userStatus?.btcAddress?.slice(0, 10)}...

Smart Account: {userStatus?.smartAccountAddress?.slice(0, 10)}...

); } return (
{error &&

{error}

}
); } ``` ## Full Example App ```typescript theme={null} // src/App.tsx import React from 'react'; import { BorrowProvider } from './contexts/BorrowContext'; import { ConnectWallet } from './components/ConnectWallet'; import { LoanForm } from './components/LoanForm'; import { PositionsList } from './components/PositionsList'; import { useBorrowContext } from './contexts/BorrowContext'; import { ChainType } from '@satsterminal-sdk/borrow'; function Dashboard() { const { isInitialized } = useBorrowContext(); if (!isInitialized) { return (

SatsTerminal Borrow

); } return (

SatsTerminal Borrow

); } export default function App() { return ( ); } ``` ## Best Practices ### 1. Error Boundaries ```typescript theme={null} class BorrowErrorBoundary extends React.Component< { children: ReactNode }, { hasError: boolean; error: Error | null } > { state = { hasError: false, error: null }; static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } render() { if (this.state.hasError) { return (

Something went wrong

{this.state.error?.message}

); } return this.props.children; } } ``` ### 2. Loading States ```typescript theme={null} function LoadingSpinner() { return
Loading...
; } function withLoading

( Component: React.ComponentType

, useLoadingHook: () => boolean ) { return function WithLoadingComponent(props: P) { const isLoading = useLoadingHook(); if (isLoading) { return ; } return ; }; } ``` ### 3. Optimistic Updates ```typescript theme={null} function useLoanWithOptimisticUpdate() { const [optimisticLoans, setOptimisticLoans] = useState([]); const startLoan = async (options: LoanOptions) => { // Add optimistic loan const optimisticLoan: UserTransaction = { id: 'temp-' + Date.now(), type: 'borrow', status: 'pending', amount: options.loanAmountUSD.toString(), currency: 'USD', timestamp: Date.now() }; setOptimisticLoans(prev => [optimisticLoan, ...prev]); try { const quotes = await sdk.getQuotes({ collateralAmount: options.collateralBTC.toString(), loanAmount: options.loanAmountUSD.toString(), }); const workflowId = await sdk.executeBorrow(quotes[0]); await sdk.trackWorkflow(workflowId, {}, 'borrow'); // Refresh real data await refreshLoans(); } finally { // Remove optimistic loan setOptimisticLoans(prev => prev.filter(l => l.id !== optimisticLoan.id) ); } }; return { optimisticLoans, startLoan }; } ``` # Configuration Source: https://developer.satsterminal.com/borrow/getting-started/configuration # Configuration The SDK is configured through the `BorrowSDKConfig` object passed to the constructor. ## Required Options These options must be provided: ```typescript theme={null} const sdk = new BorrowSDK({ // Required apiKey: 'your-api-key', wallet: { address: 'bc1q...', signMessage: async (msg) => wallet.signMessage(msg) } }); ``` ### `apiKey` Your SatsTerminal API key for authentication. | Type | Required | | -------- | -------- | | `string` | Yes | ### `wallet` The wallet provider for signing operations. | Property | Type | Required | Description | | ------------- | ----------------------------------------------- | -------- | ------------------------ | | `address` | `string` | Yes | Bitcoin address | | `signMessage` | `(msg: string) => Promise` | Yes | Message signing function | | `publicKey` | `string` | No | Public key (hex) | | `sendBitcoin` | `(to: string, sats: number) => Promise` | No | Bitcoin sending function | ```typescript theme={null} wallet: { address: 'bc1qxyz...', publicKey: '02abc...', // Optional signMessage: async (message) => { return await bitcoinWallet.signMessage(message); }, sendBitcoin: async (toAddress, satoshis) => { return await bitcoinWallet.sendBitcoin(toAddress, satoshis); } } ``` ## Optional Options ### `workflowPollInterval` Interval (in milliseconds) for polling workflow status. | Type | Default | Min | | -------- | ------- | ----- | | `number` | `2000` | `100` | ```typescript theme={null} workflowPollInterval: 3000 // Poll every 3 seconds ``` ### `sessionValiditySeconds` How long sessions remain valid (in seconds). | Type | Default | Min | | -------- | ----------------- | ---- | | `number` | `259200` (3 days) | `60` | ```typescript theme={null} sessionValiditySeconds: 86400 // 24 hours ``` ### `autoTrackWorkflows` Automatically track workflow status after operations. | Type | Default | | --------- | ------- | | `boolean` | `true` | ```typescript theme={null} autoTrackWorkflows: false // Disable auto-tracking ``` ### `quoteSelector` Custom function to select a quote from available options. | Type | Default | | ---------------------------- | ----------- | | `(quotes: Quote[]) => Quote` | First quote | ```typescript theme={null} // Select the quote with lowest APY quoteSelector: (quotes) => { return quotes.reduce((best, q) => parseFloat(q.borrowApy.variable) < parseFloat(best.borrowApy.variable) ? q : best ); } ``` ### `storage` Custom storage provider for persisting signatures. | Type | Default | | ----------------- | -------------------------------- | | `StorageProvider` | `localStorage` / `MemoryStorage` | ```typescript theme={null} // Custom storage implementation storage: { getItem: (key) => myStorage.get(key), setItem: (key, value) => myStorage.set(key, value), removeItem: (key) => myStorage.delete(key), clear: () => myStorage.clear() } ``` ### `retryConfig` Configuration for automatic request retries. | Property | Type | Default | | ---------------------- | ---------- | -------------------------------- | | `maxRetries` | `number` | `3` | | `retryDelay` | `number` | `1000` | | `retryableStatusCodes` | `number[]` | `[408, 429, 500, 502, 503, 504]` | ```typescript theme={null} retryConfig: { maxRetries: 5, retryDelay: 2000, retryableStatusCodes: [429, 500, 502, 503, 504] } ``` ### `logger` Custom logger for debugging. | Type | Default | | -------- | --------- | | `Logger` | `console` | ```typescript theme={null} logger: { debug: (msg) => myLogger.debug(msg), info: (msg) => myLogger.info(msg), warn: (msg) => myLogger.warn(msg), error: (msg) => myLogger.error(msg) } ``` ### `rpcUrl` Custom RPC URL for the selected chain. | Type | Default | | -------- | ------------- | | `string` | Chain default | ```typescript theme={null} rpcUrl: 'https://arb-mainnet.g.alchemy.com/v2/your-key' ``` ### `bundlerUrl` Custom bundler URL for ERC-4337 operations. | Type | Default | | -------- | ---------------- | | `string` | Platform default | ## Full Configuration Example ```typescript theme={null} import { BorrowSDK } from '@satsterminal-sdk/borrow'; const sdk = new BorrowSDK({ // Required apiKey: process.env.API_KEY!, wallet: { address: userBtcAddress, publicKey: userPublicKey, signMessage: async (msg) => wallet.signMessage(msg), sendBitcoin: async (to, sats) => wallet.send(to, sats) }, // Optional workflowPollInterval: 2000, sessionValiditySeconds: 259200, autoTrackWorkflows: true, quoteSelector: (quotes) => { // Select quote with best APY return quotes.sort((a, b) => parseFloat(a.borrowApy.variable) - parseFloat(b.borrowApy.variable) )[0]; }, retryConfig: { maxRetries: 3, retryDelay: 1000 }, rpcUrl: process.env.RPC_URL, bundlerUrl: process.env.BUNDLER_URL }); ``` ## Environment-Based Configuration ```typescript theme={null} const config: BorrowSDKConfig = { apiKey: process.env.SATSTERMINAL_API_KEY!, wallet: walletProvider, // More verbose logging in development logger: process.env.NODE_ENV === 'development' ? console : { debug: () => {}, info: () => {}, warn: console.warn, error: console.error } }; ``` ## Validation The SDK validates configuration on initialization: ```typescript theme={null} try { const sdk = new BorrowSDK(config); } catch (error) { if (error instanceof ConfigValidationError) { console.error('Invalid config:', error.message); // e.g., "apiKey is required and must be a non-empty string" } } ``` ### Validation Rules | Option | Rule | | ------------------------ | ------------------------- | | `apiKey` | Non-empty string | | `baseUrl` | Valid URL | | `chain` | One of `ChainType` values | | `wallet.address` | Non-empty string | | `wallet.signMessage` | Function | | `workflowPollInterval` | >= 100 | | `sessionValiditySeconds` | >= 60 | # Installation Source: https://developer.satsterminal.com/borrow/getting-started/installation # Installation ## Package manager commands **npm** ```bash theme={null} npm install @satsterminal-sdk/borrow ``` **yarn** ```bash theme={null} yarn add @satsterminal-sdk/borrow ``` **pnpm** ```bash theme={null} pnpm add @satsterminal-sdk/borrow ``` ## Requirements * **Node.js** 16.x or higher * **TypeScript** 5.0+ (recommended) * A Bitcoin wallet provider (e.g., Xverse, Leather, UniSat) ## Peer Dependencies The SDK has no external runtime dependencies. It's designed to be lightweight and work in both browser and Node.js environments. ## TypeScript Support The SDK is written in TypeScript and includes full type definitions. No additional `@types` packages are required. ```typescript theme={null} import { BorrowSDK, BorrowSDKConfig, ChainType, Quote, UserTransaction } from '@satsterminal-sdk/borrow'; ``` ## Verification Verify the installation by importing the SDK: ```typescript theme={null} import { BorrowSDK } from '@satsterminal-sdk/borrow'; console.log('SDK imported successfully'); ``` ## Next Steps Continue to [Quick Start](/borrow/getting-started/quick-start) to create your first loan. Building a React application? Continue to [UI Components](/ui-components/overview) to install editable borrowing interfaces backed by this SDK. # Quick start Source: https://developer.satsterminal.com/borrow/getting-started/quick-start # Quick Start Get your first Bitcoin-backed loan in under 5 minutes. ## Prerequisites Before you begin, ensure you have: * [ ] Installed the SDK (`npm install @satsterminal-sdk/borrow`) * [ ] An API key from SatsTerminal * [ ] A Bitcoin wallet with signing capability ## Full React integration example The official [Borrow SDK Example](https://github.com/Sats-Terminal/borrow-sdk-example/tree/main) shows how these SDK concepts fit together in a production-style Next.js application. It covers Bitcoin wallet integration, `BorrowProvider`, session restoration, quote selection, workflow tracking, loan history, management actions, repayment, collateral withdrawal, and platform-wallet withdrawal. The example composes the individual Borrow UI registry components. It intentionally does **not** use the all-in-one `BorrowApp`, so its source is useful when you want to understand or customize the SDK integration layer. ## Step 1: Initialize the SDK ```typescript theme={null} import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; const sdk = new BorrowSDK({ apiKey: 'your-api-key', wallet: { address: 'bc1qxyz...', // Your BTC address signMessage: async (message: string) => { // Implement signing with your wallet return await yourWallet.signMessage(message); } } }); ``` ## Step 2: Optional Account Setup Use `setup()` when you want to preload account state for dashboards. It is optional for borrowing because `executeBorrow()` derives the chain from the selected quote and prepares the required wallet/session state automatically: ```typescript theme={null} const { platformWallet, userStatus, activeSession } = await sdk.setup(); console.log('Smart Account:', platformWallet.address); console.log('Session valid until:', new Date(activeSession.validUntil * 1000)); ``` > `setup()` may prompt the user to sign with their Bitcoin wallet. If you skip setup, the first `executeBorrow()` call may prompt for the required signatures instead. ## Step 3: Preview Fees Before Borrowing If your UI needs to show fees up front, fetch quotes first and then request the fee breakdown for the selected quote: ```typescript theme={null} const quotes = await sdk.getQuotes({ collateralAmount: '0.1', loanAmount: '5000' }); const selectedQuote = quotes[0]; const fees = await sdk.getQuoteFees({ collateralAmount: '0.1', loanAmount: '5000', fromChain: ChainType.BITCOIN, fromAssetSymbol: 'BTC', toChain: selectedQuote.chain, toAssetSymbol: selectedQuote.collateralAssetSymbol ?? 'WBTC', loanChain: selectedQuote.loanChain ?? selectedQuote.chain, loanAssetSymbol: selectedQuote.loanAssetSymbol ?? 'USDC' }); console.log('Bridge fee USD:', fees.bridgeFees.totalBridgeFeeUSD); console.log('Platform fee:', fees.borrowFees.platformFee); console.log('Net loan to user:', fees.borrowFees.netLoanAmount); ``` ## Step 4: Execute and Track the Borrow After the user selects a quote, execute that quote and track the resulting borrow workflow: ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onStatusUpdate: (status) => { console.log(`[${status.step}] ${status.label}`); }, onDepositReady: (info) => { console.log(`\nDeposit Required:`); console.log(` Amount: ${info.amountBTC} BTC`); console.log(` Address: ${info.address}`); }, onComplete: (result) => { console.log('\nLoan Complete!'); }, onError: (error) => { console.error('Error:', error); } }, 'borrow'); console.log('Workflow ID:', workflowId); console.log('Selected Quote:', selectedQuote); ``` ## Step 5: Deposit Bitcoin When `onDepositReady` fires, send the required Bitcoin to the deposit address: Deposit addresses are single-use and the quote remains locked for up to 6 hours. If the deposit is not received before that window ends, request a fresh quote and deposit address. ```typescript theme={null} onDepositReady: async (info) => { // If your wallet supports programmatic sending: if (sdk.walletProvider.sendBitcoin) { const txHash = await sdk.sendBitcoin( info.address, Math.round(info.amountBTC * 100_000_000) // Convert to satoshis ); console.log('Deposit TX:', txHash); } else { // Otherwise, display for manual deposit console.log(`Please send ${info.amountBTC} BTC to ${info.address}`); } } ``` ## Step 6: Loan Complete Once the deposit is confirmed and processed, the loan will be executed automatically. The borrowed stablecoins will be available in your smart account. ## Complete Example ```typescript theme={null} import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; async function main() { // Initialize const sdk = new BorrowSDK({ apiKey: process.env.SATSTERMINAL_API_KEY!, wallet: { address: process.env.BTC_ADDRESS!, signMessage: async (msg) => yourWallet.signMessage(msg) } }); // Get quotes console.log('Requesting quotes...'); const quotes = await sdk.getQuotes({ collateralAmount: '0.1', loanAmount: '5000' }); const selectedQuote = quotes[0]; // Execute and track borrow console.log('Starting borrow...'); const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onStatusUpdate: (s) => console.log(`Status: ${s.label}`), onDepositReady: (i) => console.log(`Deposit ${i.amountBTC} BTC to ${i.address}`), onComplete: () => console.log('Done!'), onError: (e) => console.error(e) }, 'borrow'); } main().catch(console.error); ``` ## What's Next? * [Borrow SDK Example](https://github.com/Sats-Terminal/borrow-sdk-example/tree/main) - Complete React integration using the atomic UI components * [Configuration](configuration.md) - Customize SDK behavior * [Getting a Loan](../guides/getting-a-loan.md) - Detailed loan guide * [API Reference](../api-reference/borrow-sdk.md) - Full API documentation # Borrowing More Source: https://developer.satsterminal.com/borrow/guides/borrowing-more Increase the debt on an existing loan without adding collateral # Borrowing More `borrowMore()` increases the outstanding debt on an existing loan, reusing the collateral already deposited. The additional funds are disbursed to the loan's configured destination (chain, asset and address from the original borrow). ## When it's available Borrow More draws against the loan's remaining borrow capacity. It is rejected when the loan is: * **Fully repaid** — there is no active position to borrow against * **Liquidated** — no collateral remains * **At capacity** — the requested amount exceeds the position's available borrow room The requested amount is validated against live on-chain borrow capacity before the workflow starts. Its live USD value must also be at least **10 USD**. For USDC loans, this means the minimum Borrow More amount is 10 USDC. ## Prerequisites ```typescript theme={null} import { BorrowSDK } from '@satsterminal/sdk'; const sdk = new BorrowSDK({ apiKey: process.env.SATS_API_KEY, walletProvider, }); ``` You need the `originalBorrowId` of an active loan (from `getLoanHistory()` or the result of the original `executeBorrow()`). ## Increase the loan ```typescript theme={null} const result = await sdk.borrowMore(loanId, '500', { // disbursementFeeBps: 50, // optional payout fee override (bps) trackWorkflow: true, callbacks: { onStatusUpdate: (status) => console.log(status.label), onComplete: () => console.log('Additional funds disbursed!'), onError: (error) => console.error(error), }, }); console.log('Borrow more transaction:', result.transactionId); console.log('Workflow:', result.workflowId); ``` `borrowAmount` is denominated in the loan asset (e.g. USDC). The loan chain is inferred from the original loan; pass `options.chain` only to override it. ## Workflow stages Because the collateral already exists, borrow-more skips bridging and runs: | Stage | Description | | -------------------------- | ----------------------------------------------------------------------- | | `INITIALIZING` | Starting the workflow | | `PREPARING_BORROW_DEPOSIT` | Preparing the existing lending position; no new collateral is deposited | | `PREPARING_LOAN` | Preparing the additional borrow | | `LOAN_CONFIRMED` | Debt increased on-chain | | `PREPARING_DISBURSEMENT` | Preparing payout of the new funds | | `DISBURSEMENT_SUBMITTED` | Destination payout submitted | | `DISBURSEMENT_COMPLETED` | Funds sent to the destination | | `COMPLETED` | Workflow finished | The flow can terminate at `FAILED`, `DISBURSEMENT_FAILED`, or `CANCELLED`. ## Monitoring status If you do not use callbacks, poll the transaction directly: ```typescript theme={null} const status = await sdk.getBorrowMoreStatus(result.transactionId); console.log(status.status); // All borrow-more transactions for a loan: const history = await sdk.getBorrowMoreTransactions(loanId); ``` ## Error handling ```typescript theme={null} try { await sdk.borrowMore(loanId, '500'); } catch (error) { if (error.message.includes('available borrow room')) { // Requested amount exceeds capacity — lower the amount or add collateral first } } ``` To increase your borrow capacity before borrowing more, add collateral with [`depositMore()`](/borrow/guides/depositing-more-collateral). # Cross chain withdrawals Source: https://developer.satsterminal.com/borrow/guides/cross-chain-withdrawals # Cross-Chain Withdrawals This guide covers withdrawing assets from your EVM smart account. ## Overview The SDK provides two withdrawal methods: | Method | Destination | Gas | Use Case | | --------------------- | --------------- | -------------------- | ------------------------------------- | | `withdrawToEVM()` | EVM address | **Sponsored (Free)** | Transfer USDC to your personal wallet | | `withdrawToBitcoin()` | Bitcoin address | Paid via bridge | Convert to BTC | *** ## Gasless EVM Withdrawals (Recommended) Withdraw USDC from your smart account to any EVM address with **zero gas fees**. The SDK prepares the platform smart wallet address/signature automatically if needed. ### How It Works ```mermaid theme={null} flowchart LR SA["Smart Account
(Platform Wallet)"] WALLET["Your Wallet
(0x742d...)"] SA -- "USDC (sponsored gas)" --> WALLET ``` 1. Your borrowed USDC is in your smart account (platform wallet) 2. SDK executes transfer via ZeroDev paymaster 3. Gas is sponsored - you pay nothing 4. USDC arrives in your personal wallet ### Basic Usage ```typescript theme={null} const transactionId = await sdk.withdrawToEVM({ chain: ChainType.ARBITRUM, amount: '100', // 100 USDC destinationAddress: '0x742d...' // Your wallet }); const status = await sdk.getWithdrawStatus(transactionId); console.log('Status:', status.data.status); console.log('Transaction:', status.data.transactionDetails?.transactionHash); ``` ### Complete Example ```typescript theme={null} import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; async function withdrawToMyWallet() { const sdk = new BorrowSDK({ apiKey: process.env.API_KEY!, wallet: walletProvider }); await sdk.setup(); // Check available balance const positions = await sdk.getWalletPositions(); const usdcBalance = positions.data.find( p => p.attributes.fungible_info?.symbol === 'USDC' )?.attributes.quantity.float || 0; console.log('Available USDC:', usdcBalance); if (usdcBalance < 10) { console.log('Insufficient balance'); return; } // Withdraw to your personal wallet (gasless!) const myWallet = '0x742d35Cc6634C0532925a3b844Bc9e7595f...'; const transactionId = await sdk.withdrawToEVM({ chain: ChainType.ARBITRUM, amount: usdcBalance.toString(), destinationAddress: myWallet }); console.log('Withdrawal created:', transactionId); const status = await sdk.getWithdrawStatus(transactionId); console.log('Status:', status.data.status); } ``` ### Supported Chains | Chain | Status | | -------- | ----------- | | Arbitrum | ✅ Supported | | Base | ✅ Supported | *** ## Bitcoin Withdrawals For withdrawing to a Bitcoin address, use `withdrawToBitcoin()`. The SDK prepares the platform smart wallet address/signature automatically if needed. ### How It Works ```mermaid theme={null} flowchart LR SA["Smart Account
(Arbitrum)"] BRIDGE["Bridge
(Garden)"] BTC["Bitcoin Wallet
(bc1q...)"] SA -- "USDC" --> BRIDGE BRIDGE -- "Swap to BTC" --> BTC ``` 1. Assets are transferred from your smart account 2. Bridge protocol swaps to BTC 3. BTC is sent to your Bitcoin address ### Basic Withdrawal ```typescript theme={null} const txId = await sdk.withdrawToBitcoin({ chain: ChainType.ARBITRUM, // Source chain amount: '100', // Amount to withdraw assetSymbol: 'USDC', // Asset to withdraw btcAddress: 'bc1q...' // Destination BTC address }); console.log('Withdrawal initiated:', txId); ``` ### Tracking Withdrawal Status ```typescript theme={null} // Get status const status = await sdk.getWithdrawStatus(txId); console.log('Success:', status.success); console.log('Stage:', status.data.workflowState?.stage); console.log('Redeem TX:', status.data.workflowState?.redeemTxHash); // Full transaction details console.log('Details:', status.data); ``` ### Supported Assets | Asset | Chains | | ----- | ------------------------ | | USDC | Arbitrum, Base, Ethereum | | USDT | Arbitrum, Base, Ethereum | ### Withdrawal Parameters ```typescript theme={null} interface WithdrawToBitcoinRequest { chain: ChainType; // Source chain amount: string; // Amount to withdraw (as string) assetSymbol: string; // 'USDC' or 'USDT' btcAddress: string; // Destination BTC address } ``` #### Chain Selection ```typescript theme={null} import { ChainType } from '@satsterminal-sdk/borrow'; // Withdraw from Arbitrum await sdk.withdrawToBitcoin({ chain: ChainType.ARBITRUM, // ... }); // Withdraw from Base await sdk.withdrawToBitcoin({ chain: ChainType.BASE, // ... }); ``` ### Checking Balance Before Withdrawal ```typescript theme={null} // Get available balance const positions = await sdk.getWalletPositions(); const usdcPosition = positions.data.find( p => p.attributes.fungible_info?.symbol === 'USDC' ); const availableBalance = usdcPosition?.attributes.quantity.float || 0; console.log('Available USDC:', availableBalance); // Validate withdrawal amount const withdrawAmount = 100; if (withdrawAmount > availableBalance) { throw new Error(`Insufficient balance. Available: ${availableBalance} USDC`); } ``` ### Withdrawal Status Response ```typescript theme={null} interface WithdrawStatusResponse { success: boolean; data: { transactionId: string; status: string; workflowState?: { stage: string; transactionHash?: string; redeemTxHash?: string; // BTC transaction hash error?: string; }; transactionDetails?: { transactionHash?: string; }; }; } ``` #### Status Stages Bitcoin withdrawals use the following bridge stages: | Stage | Description | | ---------------------------- | --------------------------------------------- | | `INITIALIZING` | Withdrawal initialized | | `QUOTE_READY` | Bridge quote prepared | | `SWAP_CREATED` | Bridge order created | | `EXECUTING_APPROVAL` | Token approval being submitted, when required | | `APPROVAL_CONFIRMED` | Token approval confirmed | | `EXECUTING_INITIATE` | Bridge transaction being submitted | | `INITIATE_CONFIRMED` | Bridge transaction confirmed | | `AWAITING_BRIDGE_COMPLETION` | Waiting for destination settlement | | `BRIDGE_COMPLETED` | Bridge settlement complete | | `COMPLETED` | BTC sent | | `FAILED` | Withdrawal failed | | `CANCELLED` | Withdrawal cancelled | Direct EVM transfers use `INITIALIZING`, `VALIDATING`, `CHECKING_BALANCE`, `EXECUTING_TRANSFER`, and `COMPLETED`, with `FAILED` and `CANCELLED` as terminal error states. ### Polling for Completion ```typescript theme={null} async function waitForWithdrawal(sdk: BorrowSDK, txId: string): Promise { const maxAttempts = 60; // 30 minutes with 30s intervals let attempts = 0; while (attempts < maxAttempts) { const status = await sdk.getWithdrawStatus(txId); const state = status.data.workflowState; if (state?.stage === 'COMPLETED') { return status.data.transactionDetails?.transactionHash ?? state.redeemTxHash!; } if (state?.stage === 'FAILED') { throw new Error(state.error || 'Withdrawal failed'); } console.log(`Status: ${state?.stage ?? status.data.status}...`); await new Promise(r => setTimeout(r, 30000)); // Wait 30s attempts++; } throw new Error('Withdrawal timed out'); } // Usage const btcTxHash = await waitForWithdrawal(sdk, txId); console.log('BTC Transaction:', btcTxHash); ``` ### Fee Estimation Get estimated fees before withdrawal: ```typescript theme={null} const fees = await sdk.getFees({ chain: ChainType.ARBITRUM, collateralAmount: '0.1' // Amount in BTC equivalent }); console.log('Fee Breakdown:'); console.log(' Garden Fee:', fees.gardenFeePercent, '%'); console.log(' Bridge Fee:', fees.totalBridgeFeePercentage, '%'); console.log(' Estimated Gas:', fees.estimatedGasFee); console.log(' Total Fee (USD):', fees.totalBridgeFeeUSD); ``` ### Error Handling ```typescript theme={null} try { const txId = await sdk.withdrawToBitcoin({ chain: ChainType.ARBITRUM, amount: '100', assetSymbol: 'USDC', btcAddress: 'bc1q...' }); } catch (error) { if (error instanceof Error) { if (error.message.includes('insufficient balance')) { console.error('Not enough balance to withdraw'); } else if (error.message.includes('invalid address')) { console.error('Invalid BTC address'); } else if (error.message.includes('minimum')) { console.error('Amount below minimum withdrawal'); } else { console.error('Withdrawal error:', error.message); } } } ``` # Depositing More Collateral Source: https://developer.satsterminal.com/borrow/guides/depositing-more-collateral Add collateral to an existing loan to improve its health factor # Depositing More Collateral `depositMore()` adds collateral to an existing loan. The new collateral is bridged from Bitcoin and deposited into the same position, lowering the loan's LTV and improving its health factor. No new debt is taken. ## Prerequisites ```typescript theme={null} import { BorrowSDK } from '@satsterminal/sdk'; const sdk = new BorrowSDK({ apiKey: process.env.SATS_API_KEY, walletProvider, }); ``` You need the `originalBorrowId` of an active loan. ## Add collateral ```typescript theme={null} const result = await sdk.depositMore(loanId, '0.01', { trackWorkflow: true, callbacks: { onStatusUpdate: (status) => console.log(status.label), onDepositReady: (info) => console.log(`Send ${info.amountBTC} BTC to ${info.address}`), onComplete: () => console.log('Collateral added!'), onError: (error) => console.error(error), }, }); console.log('Deposit more transaction:', result.transactionId); ``` `collateralAmount` is denominated in BTC. Like the initial loan, the flow bridges BTC into the loan's collateral asset before depositing, so the `onDepositReady` callback surfaces the Bitcoin deposit address and amount. ## Workflow stages | Stage | Description | | ------------------------------- | ------------------------------------------- | | `INITIALIZING` | Starting the workflow | | `QUOTE_READY` | Bridge quote prepared | | `DEPOSIT_ADDRESS_READY` | Bitcoin deposit address ready | | `AWAITING_DEPOSIT` | Waiting for the BTC deposit | | `AWAITING_DEPOSIT_CONFIRMATION` | Deposit detected; waiting for confirmations | | `DEPOSIT_CONFIRMED` | Bridge deposit confirmed | | `PREPARING_BORROW_DEPOSIT` | Preparing to supply collateral | | `COLLATERAL_DEPOSITED` | Collateral added to the position | | `COMPLETED` | Workflow finished | Because no borrow occurs, deposit-more never runs a disbursement step. The flow can terminate at `FAILED`, `CANCELLED`, or `REFUND_COMPLETED`. ## Monitoring status ```typescript theme={null} const status = await sdk.getDepositMoreStatus(result.transactionId); console.log(status.status); console.log(status.transactionState?.depositTxHash); console.log( status.transactionState?.currentConfirmations, status.transactionState?.requiredConfirmations, ); // All deposit-more transactions for a loan: const history = await sdk.getDepositMoreTransactions(loanId); ``` ## Minimum amount The Bitcoin bridge requires at least `0.0001 BTC` (10,000 satoshis) per deposit. Adding collateral increases your available borrow room. Afterwards you can draw more against the position with [`borrowMore()`](/borrow/guides/borrowing-more). # Getting a loan Source: https://developer.satsterminal.com/borrow/guides/getting-a-loan # Getting a Loan This guide walks through the complete process of getting a Bitcoin-backed loan using the SDK. ## Overview The loan process involves: 1. **Quote** - Get available loan quotes 2. **Borrow** - Execute the loan; the SDK prepares wallet/session state automatically 3. **Deposit** - Send Bitcoin collateral 4. **Complete** - Receive borrowed stablecoins ## Prerequisites ```typescript theme={null} import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; const sdk = new BorrowSDK({ apiKey: 'your-api-key', wallet: { address: 'bc1q...', signMessage: async (msg) => wallet.signMessage(msg) } }); ``` ## Recommended Flow Use the explicit borrow flow so your UI can show quotes, fees, and a final review before creating the borrow workflow: ```typescript theme={null} const quotes = await sdk.getQuotes({ collateralAmount: '0.1', loanAmount: '5000' }); const selectedQuote = quotes.reduce((best, q) => parseFloat(q.borrowApy.variable) < parseFloat(best.borrowApy.variable) ? q : best ); const workflowId = await sdk.executeBorrow(selectedQuote, { destinationAddress: '0x...' }); await sdk.trackWorkflow(workflowId, { onStatusUpdate: (status) => { console.log(`[Step ${status.step}] ${status.label}`); }, onDepositReady: (info) => { console.log('\n=== DEPOSIT REQUIRED ==='); console.log(`Amount: ${info.amountBTC} BTC`); console.log(`Address: ${info.address}`); console.log('========================\n'); }, onComplete: (result) => { console.log('Loan completed successfully!'); }, onError: (error) => { console.error('Loan failed:', error); } }, 'borrow'); console.log('Workflow ID:', workflowId); console.log('Selected Quote:', selectedQuote); ``` ### Optional: Setup ```typescript theme={null} const { platformWallet, userStatus, activeSession } = await sdk.setup(); console.log('Smart Account:', userStatus.smartAccountAddress); console.log('Session expires:', new Date(activeSession.validUntil * 1000)); ``` Use `setup()` when you want to preload account state for a dashboard. You can skip it for a borrow-only path; `executeBorrow()` prepares the required platform wallet, loan wallet, and session automatically. ### Step 2: Get Quotes ```typescript theme={null} const quotes = await sdk.getQuotes({ collateralAmount: '0.1', // BTC amount as string loanAmount: '5000' // USD amount as string }); console.log(`Found ${quotes.length} quotes:`); quotes.forEach((q, i) => { console.log(`${i + 1}. ${q.protocol} - APY: ${q.borrowApy.variable}%`); }); ``` ### Step 3: Select Quote ```typescript theme={null} // Select the best quote (e.g., lowest APY) const selectedQuote = quotes.reduce((best, q) => parseFloat(q.borrowApy.variable) < parseFloat(best.borrowApy.variable) ? q : best ); console.log('Selected:', selectedQuote.protocol); ``` ### Step 3a: Fetch Fees for the Selected Quote Call `getQuoteFees()` to get the bridge fees and the borrow-side breakdown (platform fee, applied disbursement bps, campaign waiver, net loan amount): ```typescript theme={null} const fees = await sdk.getQuoteFees({ collateralAmount: '0.1', loanAmount: '5000', fromChain: ChainType.BITCOIN, fromAssetSymbol: 'BTC', toChain: selectedQuote.chain, toAssetSymbol: selectedQuote.collateralAssetSymbol ?? 'WBTC', loanChain: selectedQuote.loanChain ?? selectedQuote.chain, loanAssetSymbol: selectedQuote.loanAssetSymbol ?? 'USDC', }); console.log('Bridge fee USD:', fees.bridgeFees.totalBridgeFeeUSD); console.log('Platform fee:', fees.borrowFees.platformFee); console.log('Net loan to user:', fees.borrowFees.netLoanAmount); ``` For lower latency, fetch quotes and quote fees in parallel with `Promise.all` once your UI has both a collateral and a loan amount. ### Step 4: Start New Loan ```typescript theme={null} // Create a new loan wallet await sdk.startNewLoan(); ``` ### Step 5: Execute Borrow ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote, { destinationAddress: '0x...' // Optional }); console.log('Workflow started:', workflowId); ``` ### Step 6: Track Workflow ```typescript theme={null} await sdk.trackWorkflow(workflowId, { onStatusUpdate: (status) => console.log(status.label), onDepositReady: (info) => console.log(`Deposit ${info.amountBTC} BTC to ${info.address}`), onComplete: () => console.log('Complete!'), onError: (error) => console.error(error) }, 'borrow'); ``` ## Understanding Quotes A quote contains: ```typescript theme={null} interface Quote { collateralAmount: string; // BTC collateral loanAmount: string; // USD loan amount protocol: string; // Lending protocol (e.g., "aave") chain: ChainType; // Target chain borrowApy: { variable: string; // Variable APY stable: string; // Stable APY }; effectiveApy?: { variable: string; // Effective variable APY stable: string; // Effective stable APY }; } ``` ### Quote Selection Strategies ```typescript theme={null} // Lowest APY const lowestApyQuote = quotes.reduce((best, q) => parseFloat(q.borrowApy.variable) < parseFloat(best.borrowApy.variable) ? q : best ); // Specific protocol const aaveQuote = quotes.find(q => q.protocol === 'aave') || quotes[0]; // Highest loan amount const highestLoanQuote = quotes.reduce((best, q) => parseFloat(q.loanAmount) > parseFloat(best.loanAmount) ? q : best ); ``` ## Loan Parameters ### Collateral Amount The amount of BTC you're willing to put up as collateral: ```typescript theme={null} collateralAmount: '0.1' // 0.1 BTC ``` ### Loan Amount The USD value you want to borrow: ```typescript theme={null} loanAmount: '5000' // $5,000 ``` ### Loan-to-Value (LTV) The ratio of loan amount to collateral value. For example, borrowing `$7,000` against `$10,000` of BTC collateral is `70%` LTV. | LTV | Risk Level | Max Borrow (on \$10k collateral) | | --- | ------------ | -------------------------------- | | 50% | Conservative | \$5,000 | | 70% | Moderate | \$7,000 | | 80% | Aggressive | \$8,000 | ## Handling the Deposit When `onDepositReady` fires, you need to send BTC to the provided address: ### Manual Deposit Display the deposit info to the user: ```typescript theme={null} onDepositReady: (info) => { showModal({ title: 'Deposit Required', address: info.address, amount: `${info.amountBTC} BTC`, qrCode: generateQR(info.address) }); } ``` ### Programmatic Deposit If your wallet supports sending: ```typescript theme={null} onDepositReady: async (info) => { const satoshis = Math.round(info.amountBTC * 100_000_000); const txHash = await sdk.sendBitcoin(info.address, satoshis); console.log('Deposit TX:', txHash); } ``` ## Monitoring Progress ### Status Updates ```typescript theme={null} onStatusUpdate: (status) => { // Update UI setProgress({ step: status.step, label: status.label, description: status.description, isComplete: status.isComplete }); // Log for debugging console.log(`[${status.stage}] ${status.label}`); } ``` ### Progress Stages | Step | Stage | Description | | ---- | ------------------------------- | ------------------------------------------- | | 1 | INITIALIZING | Starting workflow | | 2 | QUOTE\_READY | Bridge quote prepared | | 3 | DEPOSIT\_ADDRESS\_READY | Address generated | | 4 | AWAITING\_DEPOSIT | Waiting for BTC | | 5 | AWAITING\_DEPOSIT\_CONFIRMATION | Deposit detected; waiting for confirmations | | 6 | DEPOSIT\_CONFIRMED | Deposit confirmed | | 7 | PREPARING\_BORROW\_DEPOSIT | Preparing collateral supply | | 8 | COLLATERAL\_DEPOSITED | Collateral supplied | | 9 | PREPARING\_LOAN | Creating loan | | 10 | LOAN\_CONFIRMED | Debt opened on-chain | | 11 | PREPARING\_DISBURSEMENT | Preparing destination payout | | 12 | DISBURSEMENT\_SUBMITTED | Payout submitted | | 13 | DISBURSEMENT\_COMPLETED | Payout confirmed | | 14 | COMPLETED | Workflow finished | Failure and recovery stages include `FAILED`, `DISBURSEMENT_FAILED`, `CANCELLED`, `REFUND_INITIATED`, and `REFUND_COMPLETED`. See [Workflows](/borrow/core-concepts/workflows) for terminal-stage behavior. ## Error Handling ```typescript theme={null} try { const quotes = await sdk.getQuotes({ collateralAmount: '0.1', loanAmount: '5000' }); const workflowId = await sdk.executeBorrow(quotes[0]); await sdk.trackWorkflow(workflowId, { onError: (error) => console.error('Workflow error:', error) }); } catch (error) { // Handle setup/execution errors if (error instanceof QuoteError) { console.error('No quotes available'); } else if (error instanceof SmartAccountError) { console.error('Wallet error:', error.message); } else if (error instanceof ApiError) { console.error('API error:', error.statusCode, error.message); } } ``` ## After the Loan Once complete, your borrowed stablecoins are in your smart account. You can: ### Check Positions ```typescript theme={null} const positions = await sdk.getWalletPositions(); positions.data.forEach(pos => { console.log(`${pos.attributes.fungible_info?.symbol}: ${pos.attributes.quantity.float}`); }); ``` ### View Loan History ```typescript theme={null} const history = await sdk.getLoanHistory({ status: 'active' }); history.transactions.forEach(tx => { console.log(`Loan: ${tx.amount} ${tx.currency} - Status: ${tx.status}`); }); ``` ### Withdraw to Bitcoin ```typescript theme={null} const txId = await sdk.withdrawToBitcoin({ chain: ChainType.ARBITRUM, amount: '1000', assetSymbol: 'USDC', btcAddress: 'bc1q...' }); ``` ## Complete Example ```typescript theme={null} import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; async function openLoan() { const sdk = new BorrowSDK({ apiKey: process.env.API_KEY!, wallet: { address: userBtcAddress, signMessage: async (msg) => wallet.signMessage(msg), sendBitcoin: async (to, sats) => wallet.send(to, sats) } }); try { const quotes = await sdk.getQuotes({ collateralAmount: '0.1', loanAmount: '5000' }); const selectedQuote = quotes[0]; const workflowId = await sdk.executeBorrow(selectedQuote, { destinationAddress: '0x...' }); await sdk.trackWorkflow(workflowId, { onStatusUpdate: (status) => { updateUI({ step: status.step, label: status.label }); }, onDepositReady: async (info) => { showDepositModal(info); // Or send automatically // await sdk.sendBitcoin(info.address, info.amount); }, onComplete: () => { showSuccessModal('Loan created successfully!'); router.push('/dashboard'); }, onError: (error) => { showErrorModal(error); } }, 'borrow'); console.log('Loan workflow started:', workflowId); } catch (error) { console.error('Failed to start loan:', error); } } ``` # Managing collateral Source: https://developer.satsterminal.com/borrow/guides/managing-collateral # Managing Collateral This guide covers collateral management including checking balances, withdrawing collateral, and understanding health factors. ## Understanding Collateral When you take a loan, your Bitcoin is locked as collateral: * **Total Collateral** - All BTC locked for the loan * **Available Collateral** - Collateral not at risk of liquidation * **Max Withdrawable** - Maximum you can withdraw safely ## Checking Collateral Info ```typescript theme={null} const loanId = 'your-loan-id'; const info = await sdk.getLoanCollateralInfo(loanId); if (info) { console.log('Collateral Information:'); console.log(' Total:', info.totalCollateral, 'BTC'); console.log(' Available:', info.availableCollateral, 'BTC'); console.log(' Max Withdrawable:', info.maxWithdrawable, 'BTC'); console.log(' Total Debt:', info.totalDebt, 'USD'); console.log(' Remaining Debt:', info.remainingDebt, 'USD'); } ``` ### LoanCollateralInfo Interface ```typescript theme={null} interface LoanCollateralInfo { totalCollateral: string; // Total BTC locked availableCollateral: string; // BTC above liquidation threshold maxWithdrawable: string; // Safe withdrawal amount totalDebt: string; // Original debt amount remainingDebt: string; // Current outstanding debt } ``` ## Withdrawing Collateral ### Prerequisites Before withdrawing: 1. Check `maxWithdrawable` amount 2. Ensure withdrawal won't trigger liquidation 3. Have a valid BTC address ready ### Basic Withdrawal You do not need to call `setup()` before withdrawing collateral. `withdrawCollateral()` prepares the platform wallet address/signature automatically if needed. ```typescript theme={null} const txId = await sdk.withdrawCollateral( loanId, '0.01', // Amount in BTC 'bc1q...', // Your BTC address { trackWorkflow: true, callbacks: { onStatusUpdate: (status) => { console.log(`[${status.step}] ${status.label}`); }, onComplete: () => { console.log('Withdrawal complete!'); }, onError: (error) => { console.error('Withdrawal failed:', error); } } } ); console.log('Transaction ID:', txId); ``` ### Maximum Safe Withdrawal ```typescript theme={null} const info = await sdk.getLoanCollateralInfo(loanId); if (parseFloat(info.maxWithdrawable) > 0) { await sdk.withdrawCollateral( loanId, info.maxWithdrawable, // Withdraw maximum safe amount 'bc1q...', { trackWorkflow: true, callbacks } ); } else { console.log('No collateral available for withdrawal'); } ``` ## Withdrawal with Repayment Withdraw collateral while repaying part of the loan: ```typescript theme={null} await sdk.repay(loanId, '1000', { // Repay $1000 collateralToWithdraw: '0.02', // Withdraw 0.02 BTC userBtcWithdrawAddress: 'bc1q...', trackWorkflow: true, callbacks: { onStatusUpdate: (status) => console.log(status.label), onComplete: () => console.log('Repaid and withdrew collateral') } }); ``` ## Withdrawal Workflow The withdrawal process involves bridging BTC from the EVM chain: ```mermaid theme={null} flowchart TD INIT["INITIALIZING"] TRANSFER["TRANSFERRING_TO_PLATFORM_WALLET"] B_INIT["BRIDGE_INITIALIZING"] QUOTE["BRIDGE_QUOTE_READY"] SWAP["BRIDGE_SWAP_CREATED"] APPROVE["BRIDGE_EXECUTING_APPROVAL"] APPROVE_DONE["BRIDGE_APPROVAL_CONFIRMED"] INITIATE["BRIDGE_EXECUTING_INITIATE"] INITIATE_DONE["BRIDGE_INITIATE_CONFIRMED"] AWAIT["BRIDGE_AWAITING_BRIDGE_COMPLETION
(10-30 minutes)"] BRIDGE_DONE["BRIDGE_COMPLETED"] DONE["COMPLETED"] INIT --> TRANSFER --> B_INIT --> QUOTE --> SWAP --> APPROVE --> APPROVE_DONE --> INITIATE --> INITIATE_DONE --> AWAIT --> BRIDGE_DONE --> DONE ``` ## Monitoring Withdrawal Status ### Track Active Withdrawal ```typescript theme={null} await sdk.withdrawCollateral(loanId, amount, address, { trackWorkflow: true, callbacks: { onStatusUpdate: (status) => { // Update UI with progress updateProgress({ stage: status.stage, step: status.step, label: status.label }); // Special handling for bridge stages if (status.stage === 'BRIDGE_AWAITING_BRIDGE_COMPLETION') { showNotification('Bridge in progress. This may take 10-30 minutes.'); } } } }); ``` ### Check Status Later ```typescript theme={null} const status = await sdk.getRepayStatus(transactionId); console.log('Current stage:', status.transactionState?.stage); console.log('Redeem TX:', status.transactionState?.redeemTxHash); ``` ## Health Factor Considerations ### What is Health Factor? Health Factor = (Collateral Value × Liquidation Threshold) / Debt | Health Factor | Status | | ------------- | ---------------- | | > 1.5 | Safe | | 1.0 - 1.5 | Caution | | \< 1.0 | Liquidation risk | ### Calculating Safe Withdrawal ```typescript theme={null} const info = await sdk.getLoanCollateralInfo(loanId); // The maxWithdrawable already accounts for health factor const safeAmount = info.maxWithdrawable; // For extra safety, withdraw less than max const conservativeAmount = (parseFloat(safeAmount) * 0.8).toFixed(8); console.log('Safe to withdraw:', conservativeAmount, 'BTC'); ``` ## Error Handling ### Common Errors ```typescript theme={null} try { await sdk.withdrawCollateral(loanId, amount, address); } catch (error) { if (error.message.includes('health factor')) { console.error('Cannot withdraw: would lower health factor too much'); } else if (error.message.includes('insufficient collateral')) { console.error('Not enough collateral to withdraw this amount'); } else if (error.message.includes('invalid address')) { console.error('Invalid BTC address provided'); } else { console.error('Withdrawal error:', error.message); } } ``` ### Validation Before Withdrawal ```typescript theme={null} async function validateWithdrawal(loanId: string, amount: string, address: string) { // Check amount const info = await sdk.getLoanCollateralInfo(loanId); if (parseFloat(amount) > parseFloat(info.maxWithdrawable)) { throw new Error(`Cannot withdraw ${amount} BTC. Max: ${info.maxWithdrawable} BTC`); } // Validate address if (!isValidBtcAddress(address)) { throw new Error('Invalid BTC address'); } return true; } ``` ## Complete Example ```typescript theme={null} import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; async function manageCollateral() { const sdk = new BorrowSDK({ apiKey: process.env.API_KEY!, wallet: walletProvider }); await sdk.setup(); // Get active loan const history = await sdk.getLoanHistory({ status: 'active' }); const loan = history.transactions[0]; if (!loan) { console.log('No active loans'); return; } // Check collateral const info = await sdk.getLoanCollateralInfo(loan.id); console.log('\n=== Collateral Status ==='); console.log(`Total Collateral: ${info.totalCollateral} BTC`); console.log(`Outstanding Debt: ${info.remainingDebt} USD`); console.log(`Max Withdrawable: ${info.maxWithdrawable} BTC`); // Withdraw if possible if (parseFloat(info.maxWithdrawable) > 0.001) { console.log('\nInitiating withdrawal...'); const withdrawAmount = (parseFloat(info.maxWithdrawable) * 0.5).toFixed(8); await sdk.withdrawCollateral( loan.id, withdrawAmount, 'bc1q...', { trackWorkflow: true, callbacks: { onStatusUpdate: (status) => { console.log(`[${status.step}] ${status.label}`); }, onComplete: () => { console.log(`\nSuccessfully withdrew ${withdrawAmount} BTC`); }, onError: (error) => { console.error('Withdrawal failed:', error); } } } ); } else { console.log('\nNo collateral available for withdrawal'); } } ``` ## Best Practices ### 1. Always Check Max Withdrawable First ```typescript theme={null} const info = await sdk.getLoanCollateralInfo(loanId); if (parseFloat(amount) > parseFloat(info.maxWithdrawable)) { throw new Error('Amount exceeds safe withdrawal limit'); } ``` ### 2. Leave a Safety Buffer ```typescript theme={null} // Withdraw 90% of max to leave buffer const safeAmount = (parseFloat(info.maxWithdrawable) * 0.9).toFixed(8); ``` ### 3. Verify BTC Address ```typescript theme={null} function isValidBtcAddress(address: string): boolean { // Basic validation - extend as needed return /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,62}$/.test(address); } ``` ### 4. Handle Bridge Delays ```typescript theme={null} onStatusUpdate: (status) => { if (status.stage.includes('BRIDGE')) { showMessage('Bridging in progress. Please wait...'); } } ``` ### 5. Monitor After Withdrawal ```typescript theme={null} // After withdrawal completes, refresh collateral info const updatedInfo = await sdk.getLoanCollateralInfo(loanId); console.log('Remaining collateral:', updatedInfo.totalCollateral); ``` # Portfolio positions Source: https://developer.satsterminal.com/borrow/guides/portfolio-positions # Portfolio & Positions This guide covers how to view your smart account portfolio and token positions. ## Overview After borrowing, your stablecoins are held in your smart account. The SDK provides methods to: * View token balances * Check portfolio value * Monitor position changes ## Getting Wallet Positions ```typescript theme={null} const positions = await sdk.getWalletPositions(); positions.data.forEach(position => { const info = position.attributes.fungible_info; const quantity = position.attributes.quantity; console.log(`${info?.symbol || 'Unknown'}: ${quantity.float}`); console.log(` Value: $${position.attributes.value}`); console.log(` Price: $${position.attributes.price}`); }); ``` ### Filtering Positions ```typescript theme={null} // Only simple positions (no complex DeFi positions) const simplePositions = await sdk.getWalletPositions({ filterPositions: 'only_simple' }); // Exclude trash/dust tokens const cleanPositions = await sdk.getWalletPositions({ filterTrash: 'only_non_trash' }); // Both filters const filtered = await sdk.getWalletPositions({ filterPositions: 'only_simple', filterTrash: 'only_non_trash' }); ``` ## Position Data Structure ```typescript theme={null} interface WalletPosition { id: string; type: string; attributes: { parent: string | null; protocol: string | null; name: string; position_type: string; quantity: { int: string; // Raw integer value decimals: number; // Token decimals float: number; // Human-readable amount numeric: string; // Precise string representation }; value: number | null; // USD value price: number; // Token price in USD changes: { absolute_1d: number | null; // 24h change in USD percent_1d: number | null; // 24h change in % } | null; fungible_info: { name: string; symbol: string; icon: { url: string } | null; flags: { verified: boolean }; implementations: Array<{ chain_id: string; address: string; decimals: number; }>; } | null; flags: { displayable: boolean; is_trash: boolean; }; updated_at: string; updated_at_block: number; }; relationships: { chain: { data: { type: string; id: string } }; }; } ``` ## Getting Portfolio Summary ```typescript theme={null} const portfolio = await sdk.getWalletPortfolio(); console.log('Portfolio Summary:'); console.log(' Total Positions:', portfolio.data.attributes.total.positions); console.log(' 24h Change:', portfolio.data.attributes.changes.percent_1d, '%'); // Distribution by chain console.log('\nBy Chain:'); Object.entries(portfolio.data.attributes.positions_distribution_by_chain) .forEach(([chain, value]) => { console.log(` ${chain}: $${value}`); }); // Distribution by type console.log('\nBy Type:'); Object.entries(portfolio.data.attributes.positions_distribution_by_type) .forEach(([type, value]) => { console.log(` ${type}: $${value}`); }); ``` ### Portfolio Data Structure ```typescript theme={null} interface WalletPortfolio { id: string; type: string; attributes: { positions_distribution_by_type: Record; positions_distribution_by_chain: Record; total: { positions: number; }; changes: { absolute_1d: number; // 24h change in USD percent_1d: number; // 24h change in % }; }; } ``` ## Common Use Cases ### Display Token Balances ```typescript theme={null} async function displayBalances(sdk: BorrowSDK) { const positions = await sdk.getWalletPositions({ filterTrash: 'only_non_trash' }); console.log('Token Balances:'); console.log('─'.repeat(50)); positions.data .filter(p => p.attributes.value && p.attributes.value > 0.01) .sort((a, b) => (b.attributes.value || 0) - (a.attributes.value || 0)) .forEach(p => { const symbol = p.attributes.fungible_info?.symbol || 'Unknown'; const amount = p.attributes.quantity.float.toFixed(4); const value = p.attributes.value?.toFixed(2) || '0.00'; console.log(`${symbol.padEnd(10)} ${amount.padStart(15)} ($${value})`); }); } ``` ### Check Specific Token Balance ```typescript theme={null} async function getTokenBalance(sdk: BorrowSDK, symbol: string): Promise { const positions = await sdk.getWalletPositions(); const token = positions.data.find( p => p.attributes.fungible_info?.symbol === symbol ); return token?.attributes.quantity.float || 0; } // Usage const usdcBalance = await getTokenBalance(sdk, 'USDC'); console.log('USDC Balance:', usdcBalance); ``` ### Calculate Total Portfolio Value ```typescript theme={null} async function getTotalValue(sdk: BorrowSDK): Promise { const positions = await sdk.getWalletPositions(); return positions.data.reduce((total, p) => { return total + (p.attributes.value || 0); }, 0); } ``` ### Monitor Position Changes ```typescript theme={null} async function checkPositionChanges(sdk: BorrowSDK) { const positions = await sdk.getWalletPositions(); const gainers: string[] = []; const losers: string[] = []; positions.data.forEach(p => { const change = p.attributes.changes?.percent_1d; if (change === null || change === undefined) return; const symbol = p.attributes.fungible_info?.symbol || 'Unknown'; if (change > 0) { gainers.push(`${symbol}: +${change.toFixed(2)}%`); } else if (change < 0) { losers.push(`${symbol}: ${change.toFixed(2)}%`); } }); console.log('Gainers:', gainers.join(', ') || 'None'); console.log('Losers:', losers.join(', ') || 'None'); } ``` ## Error Handling ```typescript theme={null} try { const positions = await sdk.getWalletPositions(); } catch (error) { if (error.message.includes('not initialized')) { console.error('Platform wallet not initialized'); await sdk.requirePlatformWallet(); return await sdk.getWalletPositions(); } throw error; } ``` ## Complete Example ```typescript theme={null} import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; async function viewPortfolio() { const sdk = new BorrowSDK({ apiKey: process.env.API_KEY!, wallet: walletProvider }); await sdk.setup(); // Get portfolio summary const portfolio = await sdk.getWalletPortfolio(); console.log('═'.repeat(50)); console.log('PORTFOLIO SUMMARY'); console.log('═'.repeat(50)); console.log(`Total Positions: ${portfolio.data.attributes.total.positions}`); console.log(`24h Change: ${portfolio.data.attributes.changes.percent_1d.toFixed(2)}%`); // Get positions const positions = await sdk.getWalletPositions({ filterTrash: 'only_non_trash' }); console.log('\n' + '─'.repeat(50)); console.log('TOKEN BALANCES'); console.log('─'.repeat(50)); let totalValue = 0; positions.data .filter(p => p.attributes.displayable !== false) .sort((a, b) => (b.attributes.value || 0) - (a.attributes.value || 0)) .forEach(p => { const symbol = p.attributes.fungible_info?.symbol || 'Unknown'; const amount = p.attributes.quantity.float; const value = p.attributes.value || 0; const change = p.attributes.changes?.percent_1d; totalValue += value; let changeStr = ''; if (change !== null && change !== undefined) { changeStr = change >= 0 ? ` (+${change.toFixed(1)}%)` : ` (${change.toFixed(1)}%)`; } console.log( `${symbol.padEnd(8)} ${amount.toFixed(4).padStart(12)} ` + `$${value.toFixed(2).padStart(10)}${changeStr}` ); }); console.log('─'.repeat(50)); console.log(`TOTAL`.padEnd(8) + `$${totalValue.toFixed(2).padStart(23)}`); console.log('═'.repeat(50)); } viewPortfolio().catch(console.error); ``` ## Best Practices ### 1. Cache Positions for UI ```typescript theme={null} // Don't fetch on every render const [positions, setPositions] = useState(null); useEffect(() => { sdk.getWalletPositions().then(setPositions); }, []); // Fetch once on mount // Refresh periodically useEffect(() => { const interval = setInterval(() => { sdk.getWalletPositions().then(setPositions); }, 30000); // Every 30 seconds return () => clearInterval(interval); }, []); ``` ### 2. Filter Dust Tokens ```typescript theme={null} const meaningfulPositions = positions.data.filter( p => (p.attributes.value || 0) > 0.01 // > $0.01 ); ``` ### 3. Handle Missing Data ```typescript theme={null} const symbol = position.attributes.fungible_info?.symbol ?? 'Unknown'; const value = position.attributes.value ?? 0; const change = position.attributes.changes?.percent_1d ?? null; ``` ### 4. Format Numbers Appropriately ```typescript theme={null} function formatAmount(amount: number, symbol: string): string { if (symbol === 'BTC' || symbol === 'WBTC') { return amount.toFixed(8); } else if (symbol === 'ETH') { return amount.toFixed(6); } else { return amount.toFixed(2); } } ``` # Repaying a loan Source: https://developer.satsterminal.com/borrow/guides/repaying-a-loan # Repaying a Loan This guide covers how to repay loans, including full repayment, partial repayment, and repayment using collateral. ## Overview Repayment options: * **Full Repayment** - Pay off the entire loan * **Partial Repayment** - Pay a portion of the loan * **Repay with Collateral** - Use your BTC collateral to repay * **Collateral Withdrawal** - Withdraw excess collateral ## Prerequisites Ensure you have an active loan: ```typescript theme={null} const history = await sdk.getLoanHistory({ status: 'active' }); const activeLoan = history.transactions[0]; console.log('Loan ID:', activeLoan.id); ``` You do not need to call `setup()` before repayment. `repay()` prepares the platform wallet address/signature automatically if needed. ## Full Repayment Repay the entire outstanding balance: ```typescript theme={null} const loanId = 'your-loan-id'; const repayAmount = '5000'; // Full amount in USD const txId = await sdk.repay(loanId, repayAmount, { trackWorkflow: true, callbacks: { onStatusUpdate: (status) => { console.log(`[${status.step}] ${status.label}`); }, onComplete: () => { console.log('Loan fully repaid!'); }, onError: (error) => { console.error('Repayment failed:', error); } } }); console.log('Repayment transaction:', txId); ``` ## Partial Repayment Pay a portion of the loan: ```typescript theme={null} const txId = await sdk.repay(loanId, '1000', { // Partial amount trackWorkflow: true, callbacks: { onStatusUpdate: (status) => console.log(status.label), onComplete: () => console.log('Partial repayment complete') } }); ``` ## Repay with Collateral Use your BTC collateral to repay the loan: ```typescript theme={null} const txId = await sdk.repay(loanId, '2000', { useCollateral: true, trackWorkflow: true, callbacks: { onStatusUpdate: (status) => console.log(status.label), onComplete: () => console.log('Repaid using collateral') } }); ``` ## Repay and Withdraw Collateral Repay and withdraw excess collateral in one transaction: ```typescript theme={null} const txId = await sdk.repay(loanId, '5000', { collateralToWithdraw: '0.02', // BTC to withdraw userBtcWithdrawAddress: 'bc1q...', // Where to send BTC trackWorkflow: true, callbacks: { onStatusUpdate: (status) => { console.log(`${status.label}`); if (status.stage.startsWith('BRIDGE_')) { console.log(' Bridging BTC to your wallet...'); } }, onComplete: () => { console.log('Repayment and withdrawal complete!'); } } }); ``` ## Checking Repayment Status ### Get Repay Transaction Status ```typescript theme={null} const status = await sdk.getRepayStatus(transactionId); console.log('Status history:', status.transactionStatuses); console.log('Workflow stage:', status.transactionState?.stage); ``` ### Get All Repay Transactions for a Loan ```typescript theme={null} const repayments = await sdk.getRepayTransactions(loanId); repayments.forEach(tx => { console.log(`Amount: ${tx.repayAmount}`); console.log(`Is Partial: ${tx.isPartialRepay}`); console.log(`Status: ${tx.transactionStatuses[tx.transactionStatuses.length - 1]?.status}`); }); ``` ## Repayment Workflow Stages | Stage | Description | | ----------------------------------- | ------------------------------- | | `INITIALIZING` | Starting repayment | | `TRANSFERRING_TO_KERNEL` | Moving funds internally | | `REPAYING_LOAN` | Executing repayment on protocol | | `WITHDRAWING_COLLATERAL` | Withdrawing BTC (if requested) | | `TRANSFERRING_TO_PLATFORM_WALLET` | Preparing for bridge | | `BRIDGE_INITIALIZING` | Starting bridge | | `BRIDGE_QUOTE_READY` | Bridge quote received | | `BRIDGE_SWAP_CREATED` | Swap created | | `BRIDGE_EXECUTING_APPROVAL` | Approving tokens | | `BRIDGE_APPROVAL_CONFIRMED` | Approval done | | `BRIDGE_EXECUTING_INITIATE` | Starting bridge tx | | `BRIDGE_INITIATE_CONFIRMED` | Bridge initiated | | `BRIDGE_AWAITING_BRIDGE_COMPLETION` | Waiting for bridge | | `BRIDGE_COMPLETED` | Bridge complete | | `COMPLETED` | All done | | `FAILED` | Failed | | `CANCELLED` | Cancelled | ## Checking Available Collateral Before withdrawing, check available collateral: ```typescript theme={null} const collateralInfo = await sdk.getLoanCollateralInfo(loanId); if (collateralInfo) { console.log('Total Collateral:', collateralInfo.totalCollateral, 'BTC'); console.log('Available:', collateralInfo.availableCollateral, 'BTC'); console.log('Max Withdrawable:', collateralInfo.maxWithdrawable, 'BTC'); console.log('Total Debt:', collateralInfo.totalDebt, 'USD'); console.log('Remaining Debt:', collateralInfo.remainingDebt, 'USD'); } ``` ## Collateral-Only Withdrawal Withdraw collateral without repaying (if health factor allows): ```typescript theme={null} const txId = await sdk.withdrawCollateral( loanId, '0.01', // Amount to withdraw 'bc1q...', // BTC address { trackWorkflow: true, callbacks: { onStatusUpdate: (s) => console.log(s.label), onComplete: () => console.log('Withdrawal complete!') } } ); ``` ## Error Handling ```typescript theme={null} try { await sdk.repay(loanId, repayAmount, { trackWorkflow: true, callbacks: { onError: (error) => { // Handle workflow errors if (error.includes('insufficient balance')) { showError('Insufficient funds to repay'); } else if (error.includes('health factor')) { showError('Cannot withdraw - would liquidate loan'); } else { showError(error); } } } }); } catch (error) { // Handle setup errors if (error instanceof ApiError) { console.error('API Error:', error.statusCode); } } ``` ## Repay Options Reference ```typescript theme={null} interface RepayOptions { // Use collateral to repay instead of wallet balance useCollateral?: boolean; // Amount of collateral to withdraw (BTC as string) collateralToWithdraw?: string; // BTC address for withdrawn collateral userBtcWithdrawAddress?: string; // Track the repayment workflow trackWorkflow?: boolean; // Workflow callbacks callbacks?: { onStatusUpdate?: (status: WorkflowStatus) => void; onComplete?: (result: any) => void; onError?: (error: string) => void; }; } ``` ## Complete Example ```typescript theme={null} import { BorrowSDK, ChainType } from '@satsterminal-sdk/borrow'; async function repayLoan() { const sdk = new BorrowSDK({ apiKey: process.env.API_KEY!, wallet: walletProvider }); await sdk.setup(); // Get active loans const history = await sdk.getLoanHistory({ status: 'active' }); const loan = history.transactions[0]; if (!loan) { console.log('No active loans'); return; } // Check collateral info const collateral = await sdk.getLoanCollateralInfo(loan.id); console.log('Outstanding debt:', collateral?.remainingDebt); console.log('Max withdrawable:', collateral?.maxWithdrawable); // Full repayment with collateral withdrawal await sdk.repay(loan.id, collateral?.remainingDebt || '0', { collateralToWithdraw: collateral?.maxWithdrawable, userBtcWithdrawAddress: 'bc1q...', trackWorkflow: true, callbacks: { onStatusUpdate: (status) => { console.log(`[${status.step}] ${status.label}`); // Show bridge progress if (status.stage.startsWith('BRIDGE_')) { console.log(' Bridging collateral to BTC...'); } }, onComplete: () => { console.log('Loan fully repaid and collateral withdrawn!'); }, onError: (error) => { console.error('Repayment failed:', error); } } }); } ``` ## Best Practices ### 1. Check Balance Before Repaying ```typescript theme={null} const positions = await sdk.getWalletPositions(); const usdcBalance = positions.data.find( p => p.attributes.fungible_info?.symbol === 'USDC' ); if (parseFloat(usdcBalance?.attributes.quantity.numeric || '0') < parseFloat(repayAmount)) { throw new Error('Insufficient USDC balance'); } ``` ### 2. Calculate Optimal Withdrawal ```typescript theme={null} const info = await sdk.getLoanCollateralInfo(loanId); const repayRatio = parseFloat(repayAmount) / parseFloat(info.totalDebt); const proportionalWithdraw = parseFloat(info.totalCollateral) * repayRatio; const safeWithdraw = Math.min(proportionalWithdraw, parseFloat(info.maxWithdrawable)); ``` ### 3. Handle Bridge Delays The bridge process can take several minutes: ```typescript theme={null} onStatusUpdate: (status) => { if (status.stage === 'BRIDGE_AWAITING_BRIDGE_COMPLETION') { showMessage('Bridging BTC... This may take 10-30 minutes.'); } } ``` ### 4. Verify Withdrawal Address Always double-check the BTC address: ```typescript theme={null} const btcAddress = 'bc1q...'; // Validate address format if (!btcAddress.match(/^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/)) { throw new Error('Invalid BTC address'); } // Confirm with user const confirmed = await confirmDialog(`Withdraw to ${btcAddress}?`); if (!confirmed) return; ``` # Webhooks Source: https://developer.satsterminal.com/borrow/guides/webhooks # Webhooks SDK webhooks notify your backend when server-side borrow workflow milestones happen. The first supported event is fired when the BTC deposit is confirmed during the SDK lending workflow. ## Configure an endpoint Webhook settings are scoped to the API key used by the SDK request. ```typescript theme={null} import { BorrowSDK, ChainType, SdkWebhookEventType, } from '@satsterminal-sdk/borrow'; const sdk = new BorrowSDK({ apiKey: process.env.SATS_TERMINAL_API_KEY!, wallet, }); await sdk.updateWebhookConfig({ enabled: true, url: 'https://api.example.com/webhooks/satsterminal', events: [SdkWebhookEventType.BTC_DEPOSIT_CONFIRMED], }); ``` Rotate the signing secret before accepting production traffic: ```typescript theme={null} const { secret } = await sdk.rotateWebhookSecret(); ``` Store the returned `secret` securely. It is only returned in the rotation response. ## Event ### `sdk.borrow.btc_deposit_confirmed` Sent after the SDK lending workflow confirms the BTC bridge deposit and receives the bridge redeem transaction hash. ```json theme={null} { "id": "SDK_BTC_DEPOSIT_CONFIRMED-", "type": "sdk.borrow.btc_deposit_confirmed", "apiVersion": "2026-05-28", "createdAt": "2026-05-28T12:00:00.000Z", "data": { "transactionId": "", "workflowId": "", "status": "DEPOSIT_CONFIRMED", "btcDepositTxHash": "", "bridgeRedeemTxHash": "", "depositAddress": "" } } ``` ## Verify signatures Each webhook request includes: | Header | Description | | ------------------------- | ------------------------------------ | | `sats-terminal-event` | Webhook event type | | `sats-terminal-delivery` | Delivery/event id | | `sats-terminal-timestamp` | Unix timestamp used in the signature | | `sats-terminal-signature` | `v1=` | Signatures are HMAC-SHA256 over `${timestamp}.${rawBody}` using your webhook secret. ```typescript theme={null} import crypto from 'node:crypto'; function verifySatsTerminalWebhook({ rawBody, timestamp, signatureHeader, secret, }: { rawBody: string; timestamp: string; signatureHeader: string; secret: string; }) { const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest('hex'); const received = signatureHeader.replace(/^v1=/, ''); return crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'), ); } ``` # Borrow Overview Source: https://developer.satsterminal.com/borrow/overview Bitcoin-backed borrowing with smart accounts, multi-chain support, and secure workflows. # What is SatsTerminal Borrow? * **Borrow stablecoins against BTC** while keeping custody. * **Smart accounts (ERC-4337)** derived from your Bitcoin wallet signature, no ETH gas needed. * **Multi-chain**: Arbitrum, Base, Ethereum (more chains are added without app changes). * **Full lifecycle**: preview fees, open loans, manage collateral, repay, withdraw, and track portfolio positions. ## Key features | Capability | Details | | ----------------------- | -------------------------------------------------------- | | Multi-chain | Arbitrum, Base, Ethereum | | Gasless UX | Sponsored transactions through the derived smart account | | Deterministic addresses | Same BTC wallet ⇒ same smart account per chain | | Workflow callbacks | Status hooks for UI updates during long-running flows | ## How it works 1. **Connect** – user signs a message with their Bitcoin wallet. 2. **Derive** – SDK creates a smart account (ERC-4337) from that signature. 3. **Authorize** – session scopes what the account can do. 4. **Borrow/manage** – deposit BTC, borrow stablecoins, repay, withdraw, rebalance collateral. ```ts theme={null} const quotes = await sdk.getQuotes({ collateralAmount: "0.1", loanAmount: "5000", }); const workflowId = await sdk.executeBorrow(quotes[0]); await sdk.trackWorkflow(workflowId, { onStatusUpdate: (s) => console.log(s.label), onDepositReady: (info) => console.log(`Send ${info.amountBTC} BTC to ${info.address}`), onComplete: () => console.log("Loan complete"), }, "borrow"); ``` ## Preview fees before checkout Use `getQuotes()` plus `getQuoteFees()` when you want to show the final fee breakdown before the user deposits BTC. ```ts theme={null} const quotes = await sdk.getQuotes({ collateralAmount: "0.1", loanAmount: "5000", }); const selectedQuote = quotes[0]; const fees = await sdk.getQuoteFees({ collateralAmount: "0.1", loanAmount: "5000", fromChain: ChainType.BITCOIN, fromAssetSymbol: "BTC", toChain: selectedQuote.chain, toAssetSymbol: selectedQuote.collateralAssetSymbol ?? "WBTC", loanChain: selectedQuote.loanChain ?? selectedQuote.chain, loanAssetSymbol: selectedQuote.loanAssetSymbol ?? "USDC", }); console.log("Bridge fee USD:", fees.bridgeFees.totalBridgeFeeUSD); console.log("Platform fee:", fees.borrowFees.platformFee); console.log("Net loan to user:", fees.borrowFees.netLoanAmount); ``` ## Next steps * Full React integration: [Borrow SDK Example](https://github.com/Sats-Terminal/borrow-sdk-example/tree/main) * Installation & config: `borrow/installation` * Quickstart: `borrow/quickstart` * Ready-made React UI: [UI Components](/ui-components/overview) * Core concepts: `borrow/core-concepts` * API reference: `borrow/api` * Troubleshooting: `borrow/troubleshooting` # Best practices Source: https://developer.satsterminal.com/borrow/resources/best-practices # Best Practices Guidelines for building robust applications with the SatsTerminal Borrow SDK. ## Initialization ### Always Call Setup First ```typescript theme={null} // Correct const sdk = new BorrowSDK(config); await sdk.setup(); const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, callbacks, 'borrow'); // Incorrect - will fail const sdk = new BorrowSDK(config); await sdk.executeBorrow(selectedQuote); // Error: not initialized ``` ### Validate Configuration ```typescript theme={null} function validateConfig(config: Partial): void { if (!config.apiKey) throw new Error('API key required'); if (!config.wallet?.address) throw new Error('Wallet address required'); if (typeof config.wallet?.signMessage !== 'function') { throw new Error('signMessage must be a function'); } } const config = { apiKey: process.env.API_KEY, // ... }; validateConfig(config); const sdk = new BorrowSDK(config as BorrowSDKConfig); ``` ### Use Environment-Specific Configuration ```typescript theme={null} const config: BorrowSDKConfig = { apiKey: process.env.SATSTERMINAL_API_KEY!, wallet: walletProvider, // Verbose logging in development only logger: process.env.NODE_ENV === 'development' ? console : { debug: () => {}, info: () => {}, warn: console.warn, error: console.error } }; ``` ## Error Handling ### Always Handle Errors ```typescript theme={null} // Bad await sdk.executeBorrow(selectedQuote); // Good try { const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, callbacks, 'borrow'); } catch (error) { handleError(error); } ``` ### Use Typed Error Handling ```typescript theme={null} import { BorrowSDKError, WalletNotConnectedError, SmartAccountError, ApiError, QuoteError, WorkflowError } from '@satsterminal-sdk/borrow'; try { const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, callbacks, 'borrow'); } catch (error) { if (error instanceof WalletNotConnectedError) { showConnectWallet(); } else if (error instanceof ApiError && error.statusCode === 429) { showRateLimitMessage(); } else if (error instanceof QuoteError) { showAdjustParameters(); } else { showGenericError(error); } } ``` ### Handle Workflow Errors Separately ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onError: (error) => { // Handle workflow-specific errors handleWorkflowError(error); } }, 'borrow').catch((error) => { // Handle setup/initialization errors handleSetupError(error); }); ``` ## Session Management ### Check Session Before Operations ```typescript theme={null} async function ensureValidSession(sdk: BorrowSDK): Promise { const { userStatus } = sdk; if (!userStatus.hasActiveSession) { await sdk.setup(); return; } const now = Date.now() / 1000; const expiry = userStatus.sessionExpiry || 0; const buffer = 300; // 5 minute buffer if (now + buffer > expiry) { await sdk.setup(); // Refresh before expiry } } // Use before operations await ensureValidSession(sdk); await sdk.getLoanHistory(); ``` ### Clear Session on Disconnect ```typescript theme={null} function handleDisconnect() { sdk.clearSession(); // Navigate to connect page } ``` ## Workflow Tracking ### Always Provide All Callbacks ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onStatusUpdate: (status) => { updateUI(status); }, onDepositReady: (info) => { showDepositModal(info); }, onComplete: () => { showSuccessMessage(); }, onError: (error) => { showErrorMessage(error); } }, 'borrow'); ``` ### Persist Workflow IDs ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote); // Store for recovery localStorage.setItem('activeWorkflow', JSON.stringify({ id: workflowId, startedAt: Date.now() })); // Clean up on completion onComplete: () => { localStorage.removeItem('activeWorkflow'); } ``` ### Resume Pending Workflows ```typescript theme={null} async function checkPendingWorkflows(sdk: BorrowSDK) { const stored = localStorage.getItem('activeWorkflow'); if (!stored) return; const { id } = JSON.parse(stored); const status = await sdk.getStatus(id); if (!status.isComplete && !status.isFailed) { await sdk.resumeLoan(id, callbacks); } else { localStorage.removeItem('activeWorkflow'); } } ``` ## Quotes ### Validate Quote Selection ```typescript theme={null} function selectBestQuote(quotes: Quote[]): Quote { if (quotes.length === 0) { throw new Error('No quotes available'); } // Sort by lowest APY const sorted = [...quotes].sort((a, b) => parseFloat(a.borrowApy.variable) - parseFloat(b.borrowApy.variable) ); return sorted[0]; } ``` ### Configure Quote Selector ```typescript theme={null} const sdk = new BorrowSDK({ ...config, quoteSelector: (quotes) => { // Custom logic const preferred = quotes.find(q => q.protocol === 'aave'); return preferred || quotes[0]; } }); ``` ## Transactions ### Validate Inputs ```typescript theme={null} async function repayLoan( sdk: BorrowSDK, loanId: string, amount: string, withdrawAddress?: string ) { // Validate loan ID if (!loanId || typeof loanId !== 'string') { throw new Error('Invalid loan ID'); } // Validate amount const numAmount = parseFloat(amount); if (isNaN(numAmount) || numAmount <= 0) { throw new Error('Invalid amount'); } // Validate BTC address if provided if (withdrawAddress && !isValidBtcAddress(withdrawAddress)) { throw new Error('Invalid BTC address'); } return sdk.repay(loanId, amount, { userBtcWithdrawAddress: withdrawAddress }); } ``` ### Check Collateral Before Withdrawal ```typescript theme={null} async function safeWithdraw( sdk: BorrowSDK, loanId: string, amount: string, address: string ) { const info = await sdk.getLoanCollateralInfo(loanId); if (!info) { throw new Error('Could not get collateral info'); } if (parseFloat(amount) > parseFloat(info.maxWithdrawable)) { throw new Error( `Amount exceeds max withdrawable (${info.maxWithdrawable} BTC)` ); } return sdk.withdrawCollateral(loanId, amount, address); } ``` ## Performance ### Cache Positions ```typescript theme={null} class PositionsCache { private cache: WalletPosition[] = []; private lastFetch = 0; private ttl = 30000; // 30 seconds async getPositions(sdk: BorrowSDK): Promise { const now = Date.now(); if (this.cache.length > 0 && now - this.lastFetch < this.ttl) { return this.cache; } const response = await sdk.getWalletPositions(); this.cache = response.data; this.lastFetch = now; return this.cache; } invalidate() { this.cache = []; this.lastFetch = 0; } } ``` ### Batch Operations ```typescript theme={null} // Instead of multiple sequential calls const [history, positions, portfolio] = await Promise.all([ sdk.getLoanHistory(), sdk.getWalletPositions(), sdk.getWalletPortfolio() ]); ``` ### Use Appropriate Poll Interval ```typescript theme={null} const sdk = new BorrowSDK({ ...config, // Adjust based on needs workflowPollInterval: 3000 // 3 seconds for less frequent updates }); ``` ## Security ### Never Log Sensitive Data ```typescript theme={null} // Bad console.log('Config:', config); // May log API key // Good console.log('Config:', { chain: config.chain, walletAddress: config.wallet.address }); ``` ### Validate BTC Addresses ```typescript theme={null} function isValidBtcAddress(address: string): boolean { // Bech32 (native segwit) if (address.startsWith('bc1')) { return /^bc1[a-zA-HJ-NP-Z0-9]{39,59}$/.test(address); } // Legacy P2PKH if (address.startsWith('1')) { return /^1[a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(address); } // Legacy P2SH if (address.startsWith('3')) { return /^3[a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(address); } return false; } ``` ### Use Secure Storage ```typescript theme={null} // For sensitive data, use encrypted storage const sdk = new BorrowSDK({ ...config, storage: { getItem: (key) => decrypt(secureStorage.get(key)), setItem: (key, value) => secureStorage.set(key, encrypt(value)), removeItem: (key) => secureStorage.delete(key), clear: () => secureStorage.clear() } }); ``` ## Testing ### Mock the SDK ```typescript theme={null} const mockSDK = { setup: jest.fn().mockResolvedValue({ platformWallet: { address: '0x...' }, userStatus: { isConnected: true } }), getQuotes: jest.fn().mockResolvedValue([mockQuote]), executeBorrow: jest.fn().mockResolvedValue('test-workflow-id'), trackWorkflow: jest.fn().mockResolvedValue(undefined) }; // Use in tests await mockSDK.setup(); expect(mockSDK.setup).toHaveBeenCalled(); ``` ### Test Error Scenarios ```typescript theme={null} it('handles API errors', async () => { mockSDK.getQuotes.mockRejectedValue( new ApiError('Server error', 500) ); await expect(getQuotesWrapper()).rejects.toThrow('Server error'); }); ``` ## Logging ### Structured Logging ```typescript theme={null} const sdk = new BorrowSDK({ ...config, logger: { debug: (msg) => logger.debug({ sdk: true, level: 'debug' }, msg), info: (msg) => logger.info({ sdk: true, level: 'info' }, msg), warn: (msg) => logger.warn({ sdk: true, level: 'warn' }, msg), error: (msg) => logger.error({ sdk: true, level: 'error' }, msg) } }); ``` ### Log Important Events ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onStatusUpdate: (status) => { logger.info({ event: 'workflow_status', workflowId, stage: status.stage, step: status.step }); }, onComplete: () => { logger.info({ event: 'loan_complete', workflowId }); }, onError: (error) => { logger.error({ event: 'loan_error', workflowId, error }); } }, 'borrow'); ``` ## Unit Handling ### Use Units for Type-Safe Conversions ```typescript theme={null} import { Units, type Satoshis, type BTC } from '@satsterminal-sdk/borrow'; // Good - type-safe conversions const collateralSats: Satoshis = Units.btcToSats("0.1"); const collateralBtc: BTC = Units.satsToBtc(10000000); // Bad - manual calculations prone to errors const sats = parseFloat("0.1") * 100000000; // No type safety ``` ### Normalize Unknown Units When handling values from API responses where units may be ambiguous: ```typescript theme={null} // Use normalizeToBtc for display function displayCollateral(value: string | number): string { const btc = Units.normalizeToBtc(value); return `${Units.formatBtc(btc, 4)} BTC`; } // Use normalizeToSats for API calls function prepareApiCall(value: string | number): number { return Units.normalizeToSats(value); } ``` ### Use ResponseNormalizer for API Responses ```typescript theme={null} import { ResponseNormalizer } from '@satsterminal-sdk/borrow'; // Handle various quote response formats const quotes = await sdk.getQuotes(params); const normalizedQuotes = ResponseNormalizer.normalizeQuotes(quotes); // Extract collateral info from complex responses const collateralInfo = ResponseNormalizer.extractLoanCollateralInfo(loanData); ``` ### Format Consistently for Display ```typescript theme={null} // Consistent formatting helpers const formatBtcAmount = (sats: number) => `${Units.formatBtc(Units.satsToBtc(sats), 4)} BTC`; const formatSatsAmount = (sats: number) => `${Units.formatSats(sats)} sats`; ``` ## Summary 1. **Initialize intentionally** - Use `setup()` for dashboard/session preload; `executeBorrow()` prepares borrow state automatically 2. **Handle all errors** - Use typed error handling 3. **Manage sessions** - Check validity, refresh proactively 4. **Track workflows** - Persist IDs, handle all callbacks 5. **Validate inputs** - Check parameters before API calls 6. **Cache when possible** - Reduce unnecessary API calls 7. **Secure sensitive data** - Don't log secrets, validate addresses 8. **Test thoroughly** - Mock SDK, test error paths 9. **Log structured data** - Track important events # Error handling Source: https://developer.satsterminal.com/borrow/resources/error-handling # Error Handling This guide covers error handling strategies and best practices for the SatsTerminal Borrow SDK. ## Error Types Overview The SDK provides typed errors for precise handling: | Error Class | When Thrown | | ------------------------- | ----------------------------------------------- | | `WalletNotConnectedError` | Wallet operations without connected wallet | | `SmartAccountError` | Smart account initialization/operation failures | | `ApiError` | API request failures | | `ConfigValidationError` | Invalid SDK configuration | | `QuoteError` | Quote-related failures | | `WorkflowError` | Workflow execution failures | ## Basic Error Handling ```typescript theme={null} import { BorrowSDK, BorrowSDKError, WalletNotConnectedError, SmartAccountError, ApiError, ConfigValidationError, QuoteError, WorkflowError } from '@satsterminal-sdk/borrow'; try { await sdk.setup(); const quotes = await sdk.getQuotes({ collateralAmount: '0.1', loanAmount: '5000' }); const workflowId = await sdk.executeBorrow(quotes[0]); await sdk.trackWorkflow(workflowId, callbacks, 'borrow'); } catch (error) { if (error instanceof WalletNotConnectedError) { // Handle wallet not connected showConnectWalletPrompt(); } else if (error instanceof SmartAccountError) { // Handle smart account issues console.error('Smart account error:', error.message); } else if (error instanceof ApiError) { // Handle API errors handleApiError(error); } else if (error instanceof QuoteError) { // Handle quote errors showMessage('No quotes available. Try different parameters.'); } else if (error instanceof WorkflowError) { // Handle workflow errors console.error('Workflow failed:', error.workflowId); } else { // Handle unknown errors console.error('Unexpected error:', error); } } ``` ## Handling API Errors API errors include HTTP status codes for precise handling: ```typescript theme={null} async function handleApiError(error: ApiError) { switch (error.statusCode) { case 400: // Bad request - validation error showValidationError(error.message); break; case 401: // Unauthorized - invalid API key showError('Invalid API key. Please check your configuration.'); break; case 403: // Forbidden showError('Access denied.'); break; case 404: // Not found showError('Resource not found.'); break; case 429: // Rate limited showError('Too many requests. Please wait.'); await delay(5000); // Retry operation break; case 500: case 502: case 503: case 504: // Server errors showError('Server error. Please try again later.'); break; default: showError(`Error: ${error.message}`); } } ``` ## Retry Strategies ### Automatic Retry Configuration ```typescript theme={null} const sdk = new BorrowSDK({ // ...config retryConfig: { maxRetries: 3, retryDelay: 1000, retryableStatusCodes: [408, 429, 500, 502, 503, 504] } }); ``` ### Manual Retry with Backoff ```typescript theme={null} async function retryWithBackoff( operation: () => Promise, maxRetries = 3, baseDelay = 1000 ): Promise { let lastError: Error | undefined; for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await operation(); } catch (error) { lastError = error as Error; // Check if error is retryable if (error instanceof ApiError) { const retryable = [408, 429, 500, 502, 503, 504]; if (!retryable.includes(error.statusCode || 0)) { throw error; // Not retryable } } else if (!(error instanceof BorrowSDKError)) { throw error; // Unknown error, don't retry } // Exponential backoff const delay = baseDelay * Math.pow(2, attempt); console.log(`Retry ${attempt + 1}/${maxRetries} in ${delay}ms...`); await new Promise(r => setTimeout(r, delay)); } } throw lastError; } // Usage const quotes = await retryWithBackoff(() => sdk.getQuotes(params)); ``` ## Workflow Error Handling Workflows have their own error handling through callbacks: ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onError: (error) => { // Error during workflow execution console.error('Workflow error:', error); if (error.includes('timeout')) { showError('Operation timed out. Your deposit may still be processing.'); } else if (error.includes('deposit')) { showError('Deposit not received. Please check your transaction.'); } else if (error.includes('bridge')) { showError('Bridge operation failed. Please contact support.'); } else { showError(`Operation failed: ${error}`); } } }, 'borrow'); ``` ## Session Error Recovery ```typescript theme={null} async function executeWithSession( sdk: BorrowSDK, operation: () => Promise ): Promise { try { return await operation(); } catch (error) { if (error instanceof SmartAccountError) { if (error.message.includes('session') || error.message.includes('expired')) { // Session expired, refresh it console.log('Session expired, refreshing...'); await sdk.setup(); return await operation(); // Retry } } throw error; } } // Usage const history = await executeWithSession(sdk, () => sdk.getLoanHistory({ status: 'active' }) ); ``` ## Global Error Handler ```typescript theme={null} class SDKErrorHandler { private sdk: BorrowSDK; private onError?: (error: BorrowSDKError) => void; private onSessionExpired?: () => void; constructor(sdk: BorrowSDK) { this.sdk = sdk; } setErrorHandler(handler: (error: BorrowSDKError) => void) { this.onError = handler; } setSessionExpiredHandler(handler: () => void) { this.onSessionExpired = handler; } async execute(operation: () => Promise): Promise { try { return await operation(); } catch (error) { if (error instanceof BorrowSDKError) { this.handleSDKError(error); throw error; } throw error; } } private handleSDKError(error: BorrowSDKError) { // Log error console.error('SDK Error:', error.toJSON()); // Check for session expiry if (error instanceof SmartAccountError && error.message.includes('session')) { this.onSessionExpired?.(); } // Call global handler this.onError?.(error); } } // Usage const errorHandler = new SDKErrorHandler(sdk); errorHandler.setErrorHandler((error) => { // Send to error tracking service errorTracker.capture(error); }); errorHandler.setSessionExpiredHandler(() => { // Redirect to login router.push('/connect'); }); // Execute operations const quotes = await errorHandler.execute(() => sdk.getQuotes(params)); ``` ## Error Logging ```typescript theme={null} function logError(error: unknown, context: Record = {}) { if (error instanceof BorrowSDKError) { const errorData = { type: error.constructor.name, code: error.code, message: error.message, context: { ...error.context, ...context }, timestamp: new Date().toISOString(), stack: error.stack }; // Log locally console.error('SDK Error:', JSON.stringify(errorData, null, 2)); // Send to monitoring service monitoring.logError(errorData); } else if (error instanceof Error) { console.error('Error:', error.message, context); monitoring.logError({ type: 'Error', message: error.message, context, timestamp: new Date().toISOString(), stack: error.stack }); } else { console.error('Unknown error:', error); } } ``` ## User-Friendly Error Messages ```typescript theme={null} function getUserMessage(error: unknown): string { if (error instanceof WalletNotConnectedError) { return 'Please connect your wallet to continue.'; } if (error instanceof SmartAccountError) { if (error.message.includes('session')) { return 'Your session has expired. Please reconnect.'; } return 'There was a problem with your account. Please try again.'; } if (error instanceof ApiError) { switch (error.statusCode) { case 401: return 'Authentication failed. Please check your API key.'; case 429: return 'Too many requests. Please wait a moment and try again.'; case 500: return 'Server error. Our team has been notified.'; default: return 'Something went wrong. Please try again.'; } } if (error instanceof QuoteError) { return 'No quotes available for these parameters. Try adjusting your loan amount or collateral.'; } if (error instanceof WorkflowError) { return 'The operation could not be completed. Please check your transaction status.'; } if (error instanceof ConfigValidationError) { return 'Configuration error. Please contact support.'; } return 'An unexpected error occurred. Please try again.'; } ``` ## Best Practices ### 1. Always Catch Errors ```typescript theme={null} // Bad await sdk.executeBorrow(selectedQuote); // Good try { const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, callbacks, 'borrow'); } catch (error) { handleError(error); } ``` ### 2. Use Type Guards ```typescript theme={null} function isBorrowSDKError(error: unknown): error is BorrowSDKError { return error instanceof BorrowSDKError; } function isApiError(error: unknown): error is ApiError { return error instanceof ApiError; } ``` ### 3. Provide Context ```typescript theme={null} try { await sdk.repay(loanId, amount); } catch (error) { logError(error, { operation: 'repay', loanId, amount, userId: currentUser.id }); throw error; } ``` ### 4. Don't Swallow Errors ```typescript theme={null} // Bad try { await sdk.setup(); } catch (error) { // Silent failure } // Good try { await sdk.setup(); } catch (error) { logError(error); showUserError(getUserMessage(error)); // Optionally re-throw } ``` ### 5. Handle Workflow Errors Separately ```typescript theme={null} const workflowId = await sdk.executeBorrow(selectedQuote); await sdk.trackWorkflow(workflowId, { onError: (error) => { // Workflow-specific error handling handleWorkflowError(error); } }, 'borrow').catch((error) => { // Setup/initialization errors handleSetupError(error); }); ``` # Troubleshooting Source: https://developer.satsterminal.com/borrow/resources/troubleshooting # Troubleshooting Common issues and their solutions when using the SatsTerminal Borrow SDK. ## Setup Issues ### "apiKey is required" **Problem:** SDK initialization fails with configuration error. **Solution:** ```typescript theme={null} // Ensure apiKey is provided and not empty const sdk = new BorrowSDK({ apiKey: process.env.API_KEY!, // Check this isn't undefined // ... }); ``` **Check:** * Environment variable is set * Variable name is correct * Value is not empty string *** ### "wallet.signMessage must be a function" **Problem:** Wallet provider configuration error. **Solution:** ```typescript theme={null} const sdk = new BorrowSDK({ wallet: { address: btcAddress, signMessage: async (message: string) => { // This must be an async function that returns a string return await yourWallet.signMessage(message); } } }); ``` *** ### "Setup failed" / Smart Account Error **Problem:** `setup()` fails with SmartAccountError. **Causes:** 1. Invalid signature from wallet 2. Network connectivity issues 3. API key issues **Solution:** ```typescript theme={null} try { await sdk.setup(); } catch (error) { if (error instanceof SmartAccountError) { console.log('Original cause:', error.cause); // Try clearing session and retry sdk.clearSession(); await sdk.setup(); } } ``` *** ## Wallet Issues ### "User rejected signature" **Problem:** User cancelled the signature request. **Solution:** ```typescript theme={null} try { await sdk.setup(); } catch (error) { if (error.message.includes('rejected') || error.message.includes('cancelled')) { showMessage('Please sign the message to continue.'); } } ``` *** ### "Invalid signature format" **Problem:** Wallet returns signature in unexpected format. **Solution:** ```typescript theme={null} wallet: { signMessage: async (message: string) => { const signature = await yourWallet.signMessage(message); // Ensure signature is base64 string if (typeof signature !== 'string') { throw new Error('Invalid signature format'); } // Some wallets return hex, convert if needed if (signature.startsWith('0x')) { return Buffer.from(signature.slice(2), 'hex').toString('base64'); } return signature; } } ``` *** ## Quote Issues ### "No quotes available" **Problem:** `getQuotes()` returns empty array or throws QuoteError. **Causes:** 1. Collateral amount too small 2. Loan amount too large for collateral 3. LTV too high 4. No liquidity available **Solution:** ```typescript theme={null} // Check your parameters const quotes = await sdk.getQuotes({ collateralAmount: '0.1', // Minimum ~0.01 BTC loanAmount: '5000' // Must fit within the collateral value }); if (quotes.length === 0) { // Try with different parameters const smallerLoanQuotes = await sdk.getQuotes({ collateralAmount: '0.1', loanAmount: '3500' }); } ``` *** ## Workflow Issues ### "Deposit not received" / Stuck on AWAITING\_DEPOSIT **Problem:** Workflow stuck waiting for deposit. **Causes:** 1. BTC not sent to deposit address 2. Transaction not confirmed 3. Wrong amount sent **Solution:** 1. Verify transaction was sent to correct address 2. Check transaction confirmations (typically need 1-3) 3. Verify amount matches exactly ```typescript theme={null} onDepositReady: (info) => { console.log('Deposit to:', info.address); console.log('Amount:', info.amountBTC, 'BTC'); console.log('Satoshis:', info.amount); // Save this info for verification saveDepositInfo(info); } ``` *** ### "Workflow timeout" **Problem:** Workflow times out without completing. **Solution:** ```typescript theme={null} // Resume tracking if interrupted const pendingWorkflowId = localStorage.getItem('workflowId'); if (pendingWorkflowId) { // Check status const status = await sdk.getStatus(pendingWorkflowId); if (!status.isComplete && !status.isFailed) { // Resume tracking await sdk.resumeLoan(pendingWorkflowId, callbacks); } } ``` *** ### "Bridge failed" **Problem:** Collateral withdrawal bridge operation fails. **Causes:** 1. Network congestion 2. Bridge liquidity issues 3. Invalid BTC address **Solution:** ```typescript theme={null} // Verify BTC address before withdrawal function validateBtcAddress(address: string): boolean { // Bech32 if (address.startsWith('bc1')) { return /^bc1[a-zA-HJ-NP-Z0-9]{39,59}$/.test(address); } // Legacy return /^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(address); } // Check status after bridge failure const status = await sdk.getRepayStatus(transactionId); console.log('Bridge state:', status.transactionState); ``` *** ## Session Issues ### "Session expired" **Problem:** Operations fail after session expires. **Solution:** ```typescript theme={null} // Check session before operations if (!sdk.userStatus.hasActiveSession || Date.now() / 1000 > (sdk.userStatus.sessionExpiry || 0)) { await sdk.setup(); // Refresh session } // Or handle in error handler try { await sdk.getLoanHistory(); } catch (error) { if (error.message.includes('session')) { await sdk.setup(); await sdk.getLoanHistory(); // Retry } } ``` *** ### "Not initialized" **Problem:** Account/session preload operations fail because account state is not initialized. **Solution:** ```typescript theme={null} // Optional: preload or refresh account/session state for dashboard flows async function initializeSDK() { const sdk = new BorrowSDK(config); await sdk.setup(); return sdk; } ``` *** ## API Issues ### "Rate limited" (429) **Problem:** Too many API requests. **Solution:** ```typescript theme={null} // Add delay between requests async function rateLimitedFetch(operation: () => Promise): Promise { try { return await operation(); } catch (error) { if (error instanceof ApiError && error.statusCode === 429) { console.log('Rate limited, waiting...'); await new Promise(r => setTimeout(r, 5000)); return await operation(); } throw error; } } // Or configure retry const sdk = new BorrowSDK({ retryConfig: { maxRetries: 3, retryDelay: 2000, retryableStatusCodes: [429] } }); ``` *** ## Storage Issues ### "Storage quota exceeded" **Problem:** Browser localStorage full. **Solution:** ```typescript theme={null} // Use custom storage with cleanup const sdk = new BorrowSDK({ storage: { _data: new Map(), getItem(key) { return this._data.get(key) || localStorage.getItem(key); }, setItem(key, value) { try { localStorage.setItem(key, value); } catch { // Fallback to memory this._data.set(key, value); } }, removeItem(key) { localStorage.removeItem(key); this._data.delete(key); }, clear() { // Only clear SDK keys const prefix = '@satsterminal/borrow'; for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); if (key?.startsWith(prefix)) { localStorage.removeItem(key); } } this._data.clear(); } } }); ``` *** ## React-Specific Issues ### "Cannot update unmounted component" **Problem:** State updates after component unmounts. **Solution:** ```typescript theme={null} useEffect(() => { let mounted = true; sdk.trackWorkflow(workflowId, { onStatusUpdate: (status) => { if (mounted) { setStatus(status); } }, onComplete: () => { if (mounted) { setComplete(true); } } }, 'borrow'); return () => { mounted = false; // Stop tracking if needed }; }, []); ``` *** ## Debug Mode Enable detailed logging: ```typescript theme={null} const sdk = new BorrowSDK({ logger: { debug: (msg) => console.debug('[SDK Debug]', msg), info: (msg) => console.info('[SDK Info]', msg), warn: (msg) => console.warn('[SDK Warn]', msg), error: (msg) => console.error('[SDK Error]', msg) } }); ``` ## Getting Help If issues persist: 1. **Check documentation** - [docs.satsterminal.com](https://docs.satsterminal.com) 2. **Search issues** - [github.com/satsterminal/sdk/issues](https://github.com/satsterminal/sdk/issues) 3. **Contact support** - [support@satsterminal.com](mailto:support@satsterminal.com) When reporting issues, include: * SDK version * Error message and stack trace * Steps to reproduce * Browser/Node.js version * Relevant configuration (without API keys) # Withdrawals Source: https://developer.satsterminal.com/borrow/withdrawals Move borrowed assets back to Bitcoin or to an EVM address (gasless). ## Withdraw to Bitcoin `withdrawToBitcoin()` prepares the platform smart wallet address/signature automatically if needed. ```ts theme={null} const txId = await borrow.withdrawToBitcoin({ chain: ChainType.ARBITRUM, amount: "100", assetSymbol: "USDC", btcAddress: "bc1..." }); ``` Check status: ```ts theme={null} const status = await borrow.getWithdrawStatus(txId); ``` ## Gasless to EVM `withdrawToEVM()` prepares the platform smart wallet address/signature automatically if needed. ```ts theme={null} const txId = await borrow.withdrawToEVM({ chain: ChainType.ARBITRUM, amount: "100", assetSymbol: "USDC", destinationAddress: "0x742d...", }); const status = await borrow.getWithdrawStatus(txId); console.log(status.data.transactionDetails?.transactionHash); ``` * Uses the platform smart account (wallet index 0) and is gas-sponsored. * No native ETH needed on the destination chain. * Returns an asynchronous withdrawal transaction ID. Poll `getWithdrawStatus()` for completion and the EVM transaction hash. If you need to withdraw from a different chain, pass that `chain`; the SDK prepares the chain-specific platform wallet signing state automatically. # Bridge API Overview Source: https://developer.satsterminal.com/bridge/api Choose a bridge type and view its API reference. # Bridge API API references are split by bridge type: * **Spark Runes API:** `bridge/spark-runes/api` * **Spark Stables API (inbound/outbound):** `bridge/spark-stables/api` # Installation Source: https://developer.satsterminal.com/bridge/getting-started/installation Install the Bridge SDK standalone or via the suite client. ## Package manager commands **npm** ```bash theme={null} npm install @satsterminal-sdk/bridge ``` **yarn** ```bash theme={null} yarn add @satsterminal-sdk/bridge ``` **pnpm** ```bash theme={null} pnpm add @satsterminal-sdk/bridge ``` **Suite (optional)** ```bash theme={null} npm install satsterminal-sdk ``` ## Requirements * **Node.js** 16+ (TypeScript 5+ recommended) * **API key** for Bridge endpoints * Optional: custom `baseUrl` if not using the default core API ## TypeScript support The SDK ships with types; no extra `@types` packages are needed. ```ts theme={null} import { BridgeSDK, createBridgeClient } from "@satsterminal-sdk/bridge"; // or via suite import { createClient } from "satsterminal-sdk"; ``` ## Verify installation ```ts theme={null} import { BridgeSDK } from "@satsterminal-sdk/bridge"; const bridge = new BridgeSDK({ apiKey: "YOUR_API_KEY" }); console.log("BridgeSDK ready", !!bridge); ``` ## Next steps * Spark Runes quickstart: `bridge/spark-runes/quickstart` * Spark Stables quickstart: `bridge/spark-stables/quickstart` * Bridge overview: `bridge/overview` * Explore APIs: `bridge/api` # Bridge Overview Source: https://developer.satsterminal.com/bridge/overview Spark Runes and Spark Stables bridge types. # SatsTerminal Bridge (beta) SatsTerminal Bridge supports multiple bridge types. Pick the flow that matches your asset and direction. ## Spark Runes (BTC to Spark and back) Bridge BTC Runes into Spark as wRunes and exit back to BTC. * Overview: `bridge/spark-runes/overview` * Quickstart: `bridge/spark-runes/quickstart` * Architecture: `bridge/spark-runes/architecture` * API reference: `bridge/spark-runes/api` ## Spark Stables (USDC to/from USDB) Quote-based stablecoin bridge for inbound USDC -> USDB on Spark and outbound USDB -> USDC to supported chains. * Overview: `bridge/spark-stables/overview` * Quickstart: `bridge/spark-stables/quickstart` * Architecture: `bridge/spark-stables/architecture` * API reference: `bridge/spark-stables/api` ## Shared setup ```bash theme={null} npm install @satsterminal-sdk/bridge # or via suite npm install satsterminal-sdk ``` ```ts theme={null} import { BridgeSDK } from "@satsterminal-sdk/bridge"; const bridge = new BridgeSDK({ apiKey: process.env.API_KEY! }); ``` # Bridge Quickstarts Source: https://developer.satsterminal.com/bridge/quickstart Pick a bridge type and follow its quickstart. # Bridge Quickstarts Choose the flow you want to implement: * **Spark Runes (BTC to Spark and back):** `bridge/spark-runes/quickstart` * **Spark Stables (USDC to/from USDB):** `bridge/spark-stables/quickstart` If you have not installed the SDK yet, start with `bridge/getting-started/installation`. # Spark Runes API Source: https://developer.satsterminal.com/bridge/spark-runes/api Bridge SDK methods for Spark Runes (BTC to Spark and back). ## Client + config ```ts theme={null} import { BridgeSDK } from "@satsterminal-sdk/bridge"; // or via suite: const { bridge } = createClient({ apiKey, bridge: true }); const bridge = new BridgeSDK({ apiKey, baseUrl }); // baseUrl optional; defaults to core API ``` * Amounts: strings (u64) to avoid precision loss. * Payloads map camelCase params to snake\_case bodies (e.g., `userPublicKey` -> `user_public_key`). * Namespace: `bridge.spark.runes` (top-level methods remain for Runes). ## Methods ### `bridge.spark.runes.getBTCDepositAddress(params)` Issue a BTC deposit address for bridging Runes to Spark. | Param | Type | Notes | | --------------- | -------- | --------------------------------------- | | `userPublicKey` | `string` | User pubkey used to derive the multisig | | `runeId` | `string` | Rune identifier (e.g., `840000:3`) | | `amount` | `string` | Requested amount (u64 string) | **Returns:** `{ address: string }` Also available as `bridge.getBTCDepositAddress(...)`. ### `bridge.spark.runes.bridgeRunes(params)` Submit a proof of BTC deposit to kick off minting on Spark. | Param | Type | Notes | | --------------- | -------- | --------------------------------------------------------------- | | `btcAddress` | `string` | User's BTC address used for the deposit | | `bridgeAddress` | `string` | Bridge-issued BTC deposit address (from `getBTCDepositAddress`) | | `txid` | `string` | Deposit transaction id | | `vout` | `number` | Output index with the rune deposit | **Returns:** `{ requestId: string }` Also available as `bridge.bridgeRunes(...)`. ### `bridge.spark.runes.getActivity(userPublicKey)` Fetch bridge activity for a user. **Returns:** `SparkRunesBridgeActivity[]` Also available as `bridge.getActivity(...)`. ### `bridge.spark.runes.getTransaction(txid)` Fetch a single bridge activity by transaction id. **Returns:** `SparkRunesBridgeActivity` Also available as `bridge.getTransaction(...)`. ## Activity shape | Field | Type | Notes | | --------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | `runeId` | `string` | Rune identifier | | `amount` | `string` | Requested amount | | `normalizedAmount` | `string` | Normalized u64 amount | | `btcDepositAddress` | `string` | Address used for BTC-side deposit | | `sparkBridgeAddress?` | `string` | Spark-side deposit address (wRunes exit) | | `status` | `"failed" \| "pending" \| "address_issued" \| "waiting_for_confirmations" \| "ready_for_mint" \| "spent" \| "minted"` | Lifecycle state | | `confirmations?` | `number` | BTC confirmations seen | | `txid?` | `string` | Deposit transaction id | | `vout?` | `number` | Deposit output index | | `wruneMetadata?` | `any` | Token metadata, if available | Use `getActivity` to poll until `minted` (Runes -> Spark) or `spent` (Spark -> Runes) depending on direction. ## Status reference | Status | Meaning | | --------------------------- | -------------------------------------------------------------- | | `pending` | Request registered, awaiting deposit info | | `address_issued` | Deposit/exit address generated | | `waiting_for_confirmations` | Deposit detected; waiting for BTC confs or Spark balance check | | `ready_for_mint` | Verification passed; mint/burn in progress | | `minted` | Spark mint completed (Runes -> Spark) | | `spent` | BTC payout built/sent (Spark -> Runes) | | `failed` | Terminal failure; retry by requesting a new address | # Spark Runes Architecture Source: https://developer.satsterminal.com/bridge/spark-runes/architecture Services, flows, signing, and trust model for the Spark Runes bridge. # Components * **Built on Distributed Lab foundations:** The bridge architecture is adapted from Distributed Lab's design ([https://x.com/distributedlab](https://x.com/distributedlab)). * **Gateway:** Orchestrates both directions, derives deposit/exit addresses via FROST DKG, and constructs Spark/BTC transactions. * **Verifiers:** Hold key shares, validate deposits, and co-sign mint/burn using threshold Schnorr signatures. * **BTC indexer:** Monitors a deposit `txid` until it reaches `N_confirm_BTC = 1` confirmation. * **Spark balance checker:** Confirms that a Spark deposit address holds the expected wRunes. * **Spark entity:** Executes Spark-side transactions once signed. * **User wallet (BTC/Spark):** Sends deposits and receives payouts. # Conventions * **wRunes:** BTKN (LRC20-like) wrapped runes on Spark. * **Signers:** `M = 3` verifiers; threshold t-of-M enforced by FROST. * **Confirmations:** `N_confirm_BTC = 1` for BTC deposits before minting. * **Amounts:** Always strings (u64) to avoid precision loss. # Address derivation & signing * Gateway runs FROST DKG to derive a multisig pubkey for each `(user_pubkey, rune_id)` pair. * Each deposit address is **tweaked with a random nonce**, producing a unique address per request; prevents address reuse and binding attacks. * An **issuer multisig** is dedicated to minting/burning each wRune; verifiers co-sign via FROST. * Key shares never combine; optional periodic DKG refresh rotates shares without exposing the master key. # Flow: Runes -> Spark (mint wRunes) 1. **Issue deposit address:** Gateway derives/tweaks `(user_pubkey, rune_id)` multisig and returns `deposit_btc_address`. 2. **User deposits runes:** Broadcasts to `deposit_btc_address`, then submits `{ txid, vout, bridgeAddress, btcAddress }` via `bridgeRunes`. 3. **Wait for confirmations:** Verifiers subscribe the `txid` with the BTC indexer; once 1 conf, they validate the rune-bearing output. 4. **Mint wRunes:** Gateway uses the issuer multisig + verifier shares to sign a Spark mint; Spark entity executes it to the Spark address associated with the user. 5. **Track status:** Progression typically goes `address_issued -> waiting_for_confirmations -> ready_for_mint -> minted` (or `failed`). # Flow: Spark -> Runes (burn wRunes, pay BTC) 1. **Issue Spark deposit address:** Gateway derives/tweaks the same multisig, returning a unique `exit_spark_address`. 2. **User deposits wRunes:** Sends the specified amount to `exit_spark_address`, then notifies the gateway. 3. **Verify Spark balance:** Verifiers call the Spark balance checker to confirm funding. 4. **Burn & build payout:** Gateway signs a burn with the issuer multisig; simultaneously selects rune UTXOs to build the BTC payout to the user. Any change is sent to a fresh deposit address derived/tweaked from the same key. 5. **Track status:** Progression typically goes `address_issued -> waiting_for_confirmations -> ready_for_mint -> spent` (or `failed`). # Security model | Threat | Mitigation | | ----------------------------- | ------------------------------------------------------------------------------- | | Signer corruption `< t` | FROST threshold: no subset below t can move funds. | | BTC re-org | Wait for 1 confirmation before minting. | | Verifier liveness | Any t-of-M online can sign; FROST hides which signers participated. | | Front-running / address reuse | Per-deposit nonce produces unique addresses; replays are invalid. | | Accounting drift | Vault invariant `sum(deposits) - sum(withdrawals)` enforced on every operation. | | Key leakage | Shares never reconstructed; DKG refresh can rotate shares. | # Operational notes * Use `getActivity` to poll by `userPublicKey`; `minted` indicates Spark mint completion, `spent` indicates BTC payout for Spark exits. * Preserve the provided `bridgeAddress`/`exit_spark_address` per request; each is single-use and bound to the requested amount/rune/user. # Spark Runes Overview Source: https://developer.satsterminal.com/bridge/spark-runes/overview Bridge BTC Runes to Spark wRunes and back. # Spark Runes Bridge Spark Runes bridges BTC Runes into Spark as wRunes (BTKN/LRC20) and lets users exit back to BTC. ## What it supports * Runes -> Spark: mint wRunes after a BTC deposit is confirmed. * Spark -> Runes: burn wRunes and build a BTC payout. * Unique deposit addresses per user + rune + nonce. * FROST threshold signing across verifiers (M=3). * 1 BTC confirmation before minting. * Amounts are strings (u64) to preserve precision. ## Key components * **Gateway:** Orchestrates both directions, derives deposit/exit addresses, and builds payout transactions. * **Verifiers:** Validate deposits and co-sign mint/burn via threshold signatures. * **BTC indexer:** Watches deposits until 1 confirmation. * **Spark balance checker:** Confirms wRune funding on Spark deposits. * **Spark entity:** Executes Spark-side transactions. ## How it works (high level) **Runes -> Spark** 1. Gateway derives a multisig for `(user_pubkey, rune_id)`, tweaks it with a random nonce, and returns a unique `deposit_btc_address`. 2. User deposits runes and reports `{ txid, vout }`. 3. Verifiers confirm via the BTC indexer, then the gateway mints wRunes using the issuer multisig; tokens land on the Spark address tied to the user. **Spark -> Runes** 1. Gateway returns a unique `exit_spark_address` derived/tweaked from the same key pair. 2. User deposits wRunes; verifiers confirm Spark balance and the gateway signs a burn with the issuer multisig. 3. Gateway assembles a BTC rune payout to the user; any change goes to a fresh deposit address derived from the same key. ## SDK entry point ```ts theme={null} const runes = bridge.spark.runes; ``` ## Next steps * Quickstart: `bridge/spark-runes/quickstart` * Architecture: `bridge/spark-runes/architecture` * API reference: `bridge/spark-runes/api` # Spark Runes Quickstart Source: https://developer.satsterminal.com/bridge/spark-runes/quickstart Request BTC deposit addresses, submit proofs, and track runes bridge status. ## 1) Install ```bash theme={null} npm install @satsterminal-sdk/bridge # or via suite npm install satsterminal-sdk ``` ## 2) Initialize the client ```ts theme={null} import { BridgeSDK } from "@satsterminal-sdk/bridge"; // or: import { createBridgeClient } from "@satsterminal-sdk/bridge"; const bridge = new BridgeSDK({ apiKey: process.env.API_KEY!, baseUrl: process.env.BRIDGE_BASE_URL, // optional override }); ``` ## 3) Runes -> Spark (mint wRunes) Request a BTC deposit address scoped to the user + rune + nonce, then submit a proof after the user deposits. ```ts theme={null} const btcDeposit = await bridge.spark.runes.getBTCDepositAddress({ userPublicKey: "0x...", runeId: "840000:3", amount: "1000000", // strings (u64) }); const { requestId } = await bridge.spark.runes.bridgeRunes({ btcAddress: "bc1...", // user's BTC address used for the deposit bridgeAddress: btcDeposit.address, // issued deposit address (single-use) txid: "", vout: 0, }); ``` The bridge waits for 1 BTC confirmation before minting. ## 4) Track activity ```ts theme={null} const activity = await bridge.spark.runes.getActivity("0x..."); activity.forEach((item) => { console.log(item.txid, item.status, item.confirmations); }); ``` Common statuses * `address_issued`: deposit/exit address generated * `waiting_for_confirmations`: deposit seen, waiting for 1 conf (BTC) or balance check (Spark) * `ready_for_mint`: verified, about to mint/burn * `minted`: Spark mint completed (Runes -> Spark) * `spent`: BTC payout built/sent (Spark -> Runes) * `pending` / `failed`: still processing or terminal failure # Spark Stables API Source: https://developer.satsterminal.com/bridge/spark-stables/api Bridge SDK methods for Spark Stables (USDC to/from USDB on Spark). ## Client + config ```ts theme={null} import { BridgeSDK } from "@satsterminal-sdk/bridge"; // or via suite: const { bridge } = createClient({ apiKey, bridge: true }); const bridge = new BridgeSDK({ apiKey, baseUrl }); // baseUrl optional; defaults to core API ``` * Amounts: strings in smallest units (6 decimals). * Namespace: `bridge.spark.stables`. ## Direction model * `direction=in` (default): USDC -> USDB (inbound). * `direction=out`: USDB -> USDC (outbound). Only `quote` requires `direction`. Other endpoints infer it from ids or tx hashes, but you can pass `direction` explicitly. ## Supported chains Inbound `sourceChain` and outbound `destinationChain` support: `solana`, `base`, `polygon`, `arbitrum`, `optimism`, `ethereum`. Outbound status responses use `source_chain: "spark"`. ## Methods ### `bridge.spark.stables.quote(params)` Request a quote and a direction-specific deposit address. | Param | Type | Notes | | --------------------- | --------------- | ----------------------------------------------- | | `direction?` | `"in" \| "out"` | Default `"in"`; use `"out"` for outbound quotes | | `sourceChain?` | `string` | Required for inbound quotes | | `destinationChain?` | `string` | Required for outbound quotes | | `destinationAddress?` | `string` | Required for outbound quotes | | `amount` | `string` | Amount in smallest units (6 decimals) | | `userSparkAddress` | `string` | Spark address | **Returns:** `{ quoteId, depositAddress, amountIn, fee, amountOut, expiresAt, sourceChain?, destinationChain?, direction? }` Notes: * Inbound `depositAddress` is an EVM/Solana address for USDC. * Outbound `depositAddress` is a Spark address for USDB. ### `bridge.spark.stables.submit(params)` Submit a transaction hash after sending funds to the deposit address. | Param | Type | Notes | | --------------- | --------------- | ------------------------------------------------ | | `quoteId` | `string` | Quote identifier | | `txHash` | `string` | Source chain tx (inbound) or Spark tx (outbound) | | `sourceAddress` | `string` | Sender address on the deposit chain | | `direction?` | `"in" \| "out"` | Optional explicit direction | **Returns:** `{ bridgeId, status, message }` ### `bridge.spark.stables.process(id)` Process or poll a bridge transaction by id. **Returns:** `{ bridgeId, status, message?, braleTransferId?, sparkTxHash?, destinationTxHash?, fastFill?, error? }` ### `bridge.spark.stables.status(params)` Fetch a bridge status by id, quote, or tx hash. | Param | Type | Notes | | ------------ | --------------- | ------------------------------------------------- | | `id?` | `string` | Bridge id | | `quoteId?` | `string` | Quote id | | `txHash?` | `string` | Source chain tx (inbound) or Spark tx (outbound) | | `chain?` | `string` | Source chain (alias: `sourceChain`, inbound only) | | `direction?` | `"in" \| "out"` | Optional explicit direction | **Returns:** `SparkStablesStatusResponse` (deposit status or quote status). ### `bridge.spark.stables.lookup(query)` Lookup a bridge by any identifier (bridge id, quote id, tx hash, Brale transfer id, Spark tx hash, destination tx hash). **Returns:** `{ matchedBy, bridge }` ### `bridge.spark.stables.history(params)` Fetch bridge history for a Spark address. | Param | Type | Notes | | ------------ | --------------- | ------------------------------ | | `address` | `string` | Spark address | | `status?` | `string` | Filter by status | | `limit?` | `number` | Pagination limit | | `offset?` | `number` | Pagination offset | | `direction?` | `"in" \| "out"` | Optional filter; omit for both | **Returns:** `{ direction, bridges, pagination }` ## Status shapes Inbound deposit status (USDC -> USDB): | Field | Type | Notes | | | ------------------- | ---------------------------------------------------------------------- | ----------------------------- | ---------------------- | | `id` | `string` | Bridge id | | | `quote_id` | `string` | Quote id | | | `status` | `"processing" \| "confirming" \| "minting" \| "completed" \| "failed"` | Bridge lifecycle state | | | `tx_hash` | `string` | Source chain tx hash | | | `source_chain` | `string` | Source chain | | | `amount_in` | `string` | USDC amount in smallest units | | | `amount_out` | `string` | USDB amount out | | | `fee` | `string` | Fee in smallest units | | | `spark_address` | `string` | Destination Spark address | | | `brale_transfer_id` | \`string | null\` | Brale transfer id | | `spark_tx_hash` | \`string | null\` | Spark transaction hash | | `error` | \`string | null\` | Error message | | `created_at` | `string` | ISO timestamp | | | `completed_at` | \`string | null\` | ISO timestamp | Outbound withdrawal status (USDB -> USDC): | Field | Type | Notes | | | --------------------- | ---------------------------------------------------------------------- | ----------------------------- | ------------------------- | | `id` | `string` | Bridge id | | | `quote_id` | `string` | Quote id | | | `status` | `"processing" \| "confirming" \| "minting" \| "completed" \| "failed"` | Bridge lifecycle state | | | `tx_hash` | `string` | Spark tx hash | | | `source_chain` | `"spark"` | Source chain | | | `destination_chain` | `string` | Destination chain | | | `destination_address` | `string` | Destination address | | | `amount_in` | `string` | USDB amount in smallest units | | | `amount_out` | `string` | USDC amount out | | | `fee` | `string` | Fee in smallest units | | | `spark_address` | `string` | Source Spark address | | | `brale_transfer_id` | \`string | null\` | Brale transfer id | | `destination_tx_hash` | \`string | null\` | Destination chain tx hash | | `error` | \`string | null\` | Error message | | `created_at` | `string` | ISO timestamp | | | `completed_at` | \`string | null\` | ISO timestamp | Quote status (before submit): | Field | Type | Notes | | ------------------- | --------------------------------------- | ------------------------------------- | | `type` | `"quote"` | Response discriminator | | `id` | `string` | Quote id | | `status` | `"pending" \| "expired" \| "completed"` | Quote lifecycle state | | `sourceChain` | `string` | Source chain (`"spark"` for outbound) | | `destinationChain?` | `string` | Destination chain for outbound quotes | | `amountIn` | `string` | Amount in smallest units | | `amountOut` | `string` | Amount out in smallest units | | `fee` | `string` | Fee in smallest units | | `expiresAt` | `string` | ISO timestamp | | `createdAt` | `string` | ISO timestamp | | `direction?` | `"in" \| "out"` | Optional direction indicator | ## Status reference | Status | Meaning | | ------------ | ------------------------------------------------------------------------- | | `processing` | Deposit submitted, waiting for verification | | `confirming` | Tx found, waiting for chain confirmations | | `minting` | Brale transfer in progress | | `completed` | USDB sent to Spark (inbound) or USDC sent to destination chain (outbound) | | `failed` | Terminal failure; request a new quote | # Spark Stables Architecture Source: https://developer.satsterminal.com/bridge/spark-stables/architecture Quote-based stablecoin bridge for inbound and outbound stables. # Spark Stables Bridge Spark Stables is a quote-based bridge that supports inbound USDC -> USDB on Spark and outbound USDB -> USDC to supported chains. # Components * **User wallet (Solana/EVM or Spark):** Sends USDC on the source chain or USDB on Spark. * **Bridge SDK (`spark.stables`):** Quote, submit, and status helpers. * **Bridge API:** Issues quotes, verifies deposits, and orchestrates minting/transfers. * **Source chain (USDC):** Receives inbound deposits. * **Spark:** Receives inbound USDB and is the source for outbound USDB. * **Brale:** Converts USDC to/from USDB and transfers to the correct network. * **Destination chain (USDC):** Receives outbound transfers. # Architecture Inbound (USDC -> USDB): ```mermaid theme={null} flowchart TD User["User Wallet
(Solana/EVM)"] SDK["Bridge SDK
(spark.stables)"] API["Bridge API"] Chain["Source Chain
(USDC)"] Brale["Brale"] Spark["Spark Network
(USDB)"] User --> SDK SDK --> API User -->|send USDC| Chain API -->|verify tx| Chain API -->|create transfer| Brale Brale -->|mint/transfer USDB| Spark ``` Outbound (USDB -> USDC): ```mermaid theme={null} flowchart TD User["User Wallet
(Spark)"] SDK["Bridge SDK
(spark.stables)"] API["Bridge API"] Spark["Spark Network
(USDB)"] Brale["Brale"] Dest["Destination Chain
(USDC)"] User --> SDK SDK --> API User -->|send USDB| Spark API -->|verify tx| Spark API -->|create transfer| Brale Brale -->|release USDC| Dest ``` # Flow: USDC -> USDB (Inbound) ```mermaid theme={null} sequenceDiagram participant User participant SDK participant Bridge participant Chain participant Brale participant Spark User->>SDK: quote({ sourceChain, amount, userSparkAddress }) SDK->>Bridge: POST /bridge/quote Bridge-->>SDK: quoteId + depositAddress User->>Chain: send USDC to depositAddress User->>SDK: submit({ quoteId, txHash, sourceAddress }) SDK->>Bridge: POST /bridge/submit Bridge-->>SDK: bridgeId + status loop poll process/status SDK->>Bridge: POST /bridge/process/:id Bridge->>Chain: verify tx + confirmations Bridge->>Brale: convert USDC -> USDB Brale->>Spark: transfer USDB Bridge-->>SDK: status update end ``` # Flow: USDB -> USDC (Outbound) ```mermaid theme={null} sequenceDiagram participant User participant SDK participant Bridge participant Spark participant Brale participant Dest User->>SDK: quote({ direction: "out", destinationChain, amount, userSparkAddress, destinationAddress }) SDK->>Bridge: POST /bridge/quote Bridge-->>SDK: quoteId + depositAddress User->>Spark: send USDB to depositAddress User->>SDK: submit({ quoteId, txHash, sourceAddress, direction: "out" }) SDK->>Bridge: POST /bridge/submit Bridge-->>SDK: bridgeId + status loop poll process/status SDK->>Bridge: POST /bridge/process/:id Bridge->>Spark: verify tx Bridge->>Brale: convert USDB -> USDC Brale->>Dest: transfer USDC Bridge-->>SDK: status update end ``` # Status model * `processing`: deposit submitted, waiting for verification. * `confirming`: tx found, waiting for chain confirmations. * `minting`: Brale transfer in progress. * `completed`: USDB sent to Spark (inbound) or USDC sent to destination chain (outbound). * `failed`: terminal failure. Quote status (when polling by `quoteId`): * `pending`: quote active, waiting for deposit. * `expired`: quote expired. * `completed`: quote fulfilled. # Operational notes * Amounts are strings in smallest units (6 decimals). * Only `quote` needs `direction`; other endpoints infer it from ids or tx hashes. * Inbound uses `sourceChain`; outbound uses `destinationChain` + `destinationAddress`. * Quotes expire; the deposit address is bound to the quote. * Processing is idempotent; it is safe to retry `process`. * Supported chains: `solana`, `base`, `polygon`, `arbitrum`, `optimism`, `ethereum`. # Spark Stables Overview Source: https://developer.satsterminal.com/bridge/spark-stables/overview Quote-based stablecoin bridge between USDC and USDB on Spark. # Spark Stables Bridge Spark Stables is a quote-based bridge that supports both directions: * Inbound: USDC on supported chains -> USDB on Spark (default). * Outbound: USDB on Spark -> USDC on a destination chain. ## What it supports * Inbound USDC -> USDB on Spark via Brale. * Outbound USDB -> USDC on supported chains via Brale. * Quote-based deposits with direction-specific deposit addresses. * Chain confirmations enforced per chain. * Status tracking from processing to completed. * Amounts are strings in smallest units (6 decimals). ## Supported chains Inbound `sourceChain` / outbound `destinationChain`: `solana`, `base`, `polygon`, `arbitrum`, `optimism`, `ethereum`. Spark is the source chain for outbound. ## How it works (high level) Inbound (USDC -> USDB) 1. User requests a quote with `{ sourceChain, amount, userSparkAddress }` and receives a deposit address. 2. User sends USDC to the deposit address and submits the transaction hash. 3. Bridge verifies confirmations, orchestrates the Brale transfer, and marks the bridge `completed` once USDB lands on Spark. Outbound (USDB -> USDC) 1. User requests a quote with `{ direction: "out", destinationChain, amount, userSparkAddress, destinationAddress }`. 2. User sends USDB on Spark to the deposit address and submits the Spark transaction hash. 3. Bridge verifies the Spark tx, orchestrates the Brale transfer, and marks the bridge `completed` once USDC lands on the destination chain. ## SDK entry point ```ts theme={null} const stables = bridge.spark.stables; ``` ## Next steps * Quickstart: `bridge/spark-stables/quickstart` * Architecture: `bridge/spark-stables/architecture` * API reference: `bridge/spark-stables/api` # Spark Stables Quickstart Source: https://developer.satsterminal.com/bridge/spark-stables/quickstart Quote deposits and withdrawals, submit proofs, and track stablecoin bridge status. ## 1) Install ```bash theme={null} npm install @satsterminal-sdk/bridge # or via suite npm install satsterminal-sdk ``` ## 2) Initialize the client ```ts theme={null} import { BridgeSDK } from "@satsterminal-sdk/bridge"; // or: import { createBridgeClient } from "@satsterminal-sdk/bridge"; const bridge = new BridgeSDK({ apiKey: process.env.API_KEY!, baseUrl: process.env.BRIDGE_BASE_URL, // optional override }); ``` ## 3) USDC -> USDB (Inbound) Request a quote, send USDC, then submit the transaction and poll status. ```ts theme={null} const quote = await bridge.spark.stables.quote({ sourceChain: "solana", amount: "1000000", // USDC in smallest units (6 decimals) userSparkAddress: "spark1...", }); // User sends USDC to quote.depositAddress on the source chain. const { bridgeId } = await bridge.spark.stables.submit({ quoteId: quote.quoteId, txHash: "", sourceAddress: "", }); const status = await bridge.spark.stables.process(bridgeId); // or: await bridge.spark.stables.status({ id: bridgeId }); ``` ## 4) USDB -> USDC (Outbound) Request an outbound quote, send USDB on Spark, then submit the Spark transaction and poll status. ```ts theme={null} const quote = await bridge.spark.stables.quote({ direction: "out", destinationChain: "base", destinationAddress: "0x...", amount: "1000000", userSparkAddress: "spark1...", }); // User sends USDB on Spark to quote.depositAddress. const { bridgeId } = await bridge.spark.stables.submit({ quoteId: quote.quoteId, txHash: "", sourceAddress: "spark1...", direction: "out", }); const status = await bridge.spark.stables.process(bridgeId); ``` ## 5) Track status ```ts theme={null} const status = await bridge.spark.stables.status({ id: bridgeId }); console.log(status); ``` Common statuses * `processing`: deposit submitted, waiting for verification * `confirming`: tx found, waiting for chain confirmations * `minting`: Brale transfer in progress * `completed`: USDB sent to Spark (inbound) or USDC sent to the destination chain (outbound) * `failed`: terminal failure # Earn API Reference Source: https://developer.satsterminal.com/earn/api EarnSDK methods and types. **Coming soon** – The Earn SDK is in development. # EarnSDK ## Setup | Method | Description | | ----------------------------- | ---------------------------------------------------------------------- | | `setup()` | Derive smart account, authorize session. Call before deposit/withdraw. | | `useSmartAccount(userConfig)` | Use existing smart account (e.g. from Borrow SDK). | ## Pools | Method | Description | | --------------------- | ------------------------------------------------- | | `getPools(params?)` | List pools. Optional: `collateralToken`, `chain`. | | `getPoolById(poolId)` | Get pool by ID. | ## Estimation | Method | Description | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `getDepositEstimation(params)` | Deposit cost. Returns `DepositEstimation`. Params: `depositToken`, `collateralToken`, `collateralChain`, `amount?`, `depositChain?`. | | `getWithdrawEstimation(params)` | Withdrawal estimate. Returns `WithdrawEstimation`. Params: `amount`, `collateralToken`, `depositToken`, `protocol`, `transactionType?`, `collateralChain?`. | ## Deposits | Method | Description | | --------------------------------- | ---------------------------------------------------------------------------------------------------- | | `createDeposit(params)` | Create deposit. Requires `poolId`, `depositAmount`, `depositToken`, `collateralToken`, `userConfig`. | | `getDepositStatus(transactionId)` | Get deposit status and withdrawals. | | `getDeposits(params?)` | List deposits. Optional: `page`, `limit`. | ## Withdrawals | Method | Description | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `createWithdraw(params)` | Create withdrawal. Requires `earnTransactionId`, `withdrawAmount`. Optional: `transactionType`, `userBitcoinAddress`. | | `getWithdrawStatus(transactionId)` | Get withdrawal status. | | `getWithdrawals(params?)` | List withdrawals. Optional: `page`, `limit`. | ## Types ```ts theme={null} import { ChainType, EarnWithdrawalType, type EarnPool, type EarnTransaction, type DepositEstimation, type WithdrawEstimation, type CreateDepositParams, type CreateWithdrawParams, } from "@satsterminal-sdk/earn"; ``` * `EarnWithdrawalType`: `INSTANT` | `STANDARD` — Most protocols: instant is 0% fee. Everstake (HYSP): STANDARD = 0% fee, \~3 days; INSTANT = 0.5% fee. * `ChainType`: `BASE`, `ETHEREUM`, `ARBITRUM`, `BITCOIN`, `POLYGON`, `OPTIMISM`, `BSC`, etc. * `DepositEstimation`: `totalCostUsd?`, `bridgeFeeUsd?`, `gasEstimateUsd?` * `WithdrawEstimation`: `withdrawAmount?`, `receiveAmount?`, `feeUsd?` # Earn Overview Source: https://developer.satsterminal.com/earn/overview Deposit stablecoins or BTC into yield protocols via the SDK. **Coming soon** – The Earn SDK is in development. Documentation and API may change. # What is SatsTerminal Earn? * **Earn yield** on USDC, USDT, wBTC, cbBTC, or BTC across supported protocols. * **Same auth as Borrow** – API key + Bitcoin wallet signature; smart accounts derived from your BTC wallet. * **Protocols**: Morpho, Steakhouse, Arcadia, HYSP (Everstake). * **Chains**: Ethereum, Base, Arbitrum, Polygon, Optimism, BNB Chain, Bitcoin, and more. ## Key features | Capability | Details | | ----------------- | ----------------------------------------------------------- | | Multi-protocol | Morpho, Steakhouse, Arcadia, HYSP | | Gasless UX | Sponsored transactions via smart account | | BTC deposits | Bridge BTC via Garden; receive yield in USDC or wrapped BTC | | Same session flow | Reuse Borrow SDK session if you use both | ## How it works 1. **Setup** – sign with your Bitcoin wallet; SDK derives smart account and authorizes session. 2. **Deposit** – choose a pool, amount, and token; workflow handles bridge/swap if needed. 3. **Withdraw** – request withdrawal; choose instant or standard (3-day) for some protocols. ## Next steps * [Quickstart](/earn/quickstart) – get a deposit running in minutes * [API Reference](/earn/api) – methods and types # Earn Quickstart Source: https://developer.satsterminal.com/earn/quickstart Deposit and withdraw in a few steps. **Coming soon** – The Earn SDK is in development. ## 1) Install ```bash theme={null} npm install @satsterminal-sdk/earn @satsterminal-sdk/core ``` ## 2) Initialize ```ts theme={null} import { EarnSDK, ChainType } from "@satsterminal-sdk/earn"; const earn = new EarnSDK({ apiKey: process.env.API_KEY!, chain: ChainType.BASE, wallet: { address: "bc1q...", signMessage: async (msg) => myBtcWallet.signMessage(msg), }, }); ``` ## 3) Setup (derive smart account + session) ```ts theme={null} const { smartAccountAddress, userConfig } = await earn.setup(); console.log("Smart account:", smartAccountAddress); ``` ## 4) List pools and deposit ```ts theme={null} const pools = await earn.getPools({ collateralToken: "USDC", chain: ChainType.BASE }); const pool = pools[0]; const tx = await earn.createDeposit({ poolId: pool.id, depositAmount: "100", depositToken: "USDC", collateralToken: "USDC", userConfig, }); console.log("Deposit tx:", tx._id); ``` ## 5) Check status and withdraw ```ts theme={null} const status = await earn.getDepositStatus(tx._id); // When ready to withdraw const withdrawal = await earn.createWithdraw({ earnTransactionId: tx._id, withdrawAmount: "50", }); ``` ## Pool ID format `{protocol}-{asset}-{chain}` — e.g. `morpho-usdc-base`, `steakhouse-wbtc-ethereum`. ## Next steps * [Overview](/earn/overview) – features and flow * [API Reference](/earn/api) – full method list # Getting Started Source: https://developer.satsterminal.com/getting-started Install the SDK, initialize with your apiKey, and make your first call. ## Install ```bash theme={null} npm install satsterminal-sdk # or swaps only npm install @satsterminal-sdk/swaps # or borrow only npm install @satsterminal-sdk/borrow ``` ## Initialize (suite) ```ts theme={null} import { createClient } from "satsterminal-sdk"; import { ChainType } from "@satsterminal-sdk/borrow"; const { swaps, borrow } = createClient({ apiKey: process.env.API_KEY!, borrow: { chain: ChainType.BASE, wallet: /* your wallet provider */ } }); ``` ## First requests * Swaps: `await swaps.popularTokens({});` or `await swaps.swapQuote({...});` * Borrow: `await borrow.setup();` then `await borrow.startNewLoan();` ## Node/TypeScript TypeScript is supported out of the box. For Node, use v18+ to align with `fetch` and current toolchain. # Overview Source: https://developer.satsterminal.com/overview Modular SatsTerminal SDK packages and how they fit together. # SatsTerminal SDK The SDK is modular. You can install the all-in-one package or pick individual products: * `satsterminal-sdk` (suite): exports the legacy swaps client (`SatsTerminal`) and factories for swaps/borrow/bridge via `createClient`. * `@satsterminal-sdk/swaps`: swaps-only package. * `@satsterminal-sdk/borrow`: borrow-only package. * `@satsterminal-sdk/bridge`: bridge BTC Runes to Spark wRunes and back, plus USDC -> USDB on Spark. ### Installation options ```bash theme={null} # Suite (recommended for multiple products) npm install satsterminal-sdk # Swaps only npm install @satsterminal-sdk/swaps # Borrow only npm install @satsterminal-sdk/borrow # Bridge only npm install @satsterminal-sdk/bridge ``` ### Minimal examples Suite with shared apiKey: ```ts theme={null} import { createClient } from "satsterminal-sdk"; import { ChainType } from "@satsterminal-sdk/borrow"; const { swaps, borrow, bridge } = createClient({ apiKey: process.env.API_KEY!, borrow: { chain: ChainType.BASE, wallet }, bridge: true, }); await bridge.spark.runes.getBTCDepositAddress({ userPublicKey: "0x...", runeId: "840000:3", amount: "1000000", }); ``` Swaps-only (legacy surface): ```ts theme={null} import { SatsTerminal } from "satsterminal-sdk"; const swaps = new SatsTerminal({ apiKey: process.env.API_KEY! }); ``` Borrow-only: ```ts theme={null} import { BorrowSDK, ChainType } from "@satsterminal-sdk/borrow"; const borrow = new BorrowSDK({ apiKey: process.env.API_KEY!, chain: ChainType.ARBITRUM, wallet }); ``` # Suite usage Source: https://developer.satsterminal.com/suite/usage Use one apiKey to access swaps, borrow, and bridge from a single entrypoint. ## createClient ```ts theme={null} import { createClient } from "satsterminal-sdk"; import { ChainType } from "@satsterminal-sdk/borrow"; const { swaps, borrow, bridge } = createClient({ apiKey: process.env.API_KEY!, borrow: { chain: ChainType.BASE, wallet: /* your wallet provider */ }, bridge: true, // omit to skip initialization }); ``` ### Swaps via suite ```ts theme={null} const quote = await swaps.swapQuote({ amount: "0.001", fromToken: "BTC", toToken: "USDC", address: "bc1...", protocol: "runes", params: {} }); ``` ### Borrow via suite ```ts theme={null} const setup = await borrow.setup(); const { userStatus } = setup; // Start a new loan wallet (advanced) await borrow.startNewLoan(); ``` ### Bridge via suite ```ts theme={null} const btcDeposit = await bridge.spark.runes.getBTCDepositAddress({ userPublicKey: "0x...", runeId: "840000:3", amount: "1000000", }); const { requestId } = await bridge.spark.runes.bridgeRunes({ btcAddress: "bc1...", bridgeAddress: btcDeposit.address, txid: "", vout: 0, }); ``` Spark Stables quote (USDC -> USDB on Spark): ```ts theme={null} const quote = await bridge.spark.stables.quote({ sourceChain: "solana", amount: "1000000", userSparkAddress: "spark1...", }); ``` ### Legacy compatibility Existing swaps partners can keep: ```ts theme={null} import { SatsTerminal } from "satsterminal-sdk"; const swaps = new SatsTerminal({ apiKey: process.env.API_KEY! }); ``` # API Reference Source: https://developer.satsterminal.com/swaps/api Full V2 and V1 method reference, parameters, and example flows. # V2 (recommended) ### `swapQuote(params: SwapV2QuoteParams)` Get a quote with normalized token direction. | Param | Type | Notes | | --------------- | --------------------- | --------------------------- | | `amount` | `string` | Amount in `fromToken` units | | `fromToken` | `string` | e.g., `BTC` | | `toToken` | `string` | e.g., `USDC`, `GOLD DUST` | | `address` | `string` | User BTC address | | `protocol` | `string` | `runes`, `alkanes`, ... | | `params` | `Record` | Protocol-specific extras | | `marketplaces?` | `string[]` | Optional allowlist | ```ts theme={null} const quote = await swaps.swapQuote({ amount: "0.00008", fromToken: "BTC", toToken: "GOLD DUST", address: "bc1p...", params: {}, protocol: "alkanes", }); // quote.bestMarketplace, quote.swapId, quote.metrics, quote.marketplaces ``` **Response highlights** * `bestMarketplace`: string * `swapId`: string * `fromTokenAmount` / `toTokenAmount`: strings * `metrics`: per-marketplace fulfillment and pricing * `marketplaces`: per-marketplace amounts + swapIds ### `swapPSBT(params: SwapV2PSBTParams)` Create PSBT(s) for the swap; returns an array because some swaps need multiple signatures. | Param | Type | Notes | | ------------------ | ---------------- | ----------------------------- | | `marketplace` | `string` | From the quote | | `swapId` | `string` | From the quote/previous step | | `address` | `string` | User BTC address | | `publicKey` | `string` | BTC pubkey matching `address` | | `paymentAddress` | `string` | Change/receive address | | `paymentPublicKey` | `string` | Pubkey for `paymentAddress` | | `feeRate` | `number` | sats/vByte | | `slippage` | `number` | percent | | `protocol` | `string` | e.g., `runes` | | `themeID?` | `string \| null` | Optional widget theme | ```ts theme={null} const psbt = await swaps.swapPSBT({ marketplace: quote.bestMarketplace, swapId: quote.swapId, address: "bc1p...", publicKey: "...", paymentAddress: "...", paymentPublicKey: "...", feeRate: 3, slippage: 9, themeID: null, protocol: "alkanes", }); // psbt.psbts: [{ base64, hex, inputs }] ``` ### `swapSubmit(params: SwapV2SubmitParams)` Submit signed PSBTs to complete the swap. | Param | Type | Notes | | ------------------ | ---------- | ----------------------- | | `marketplace` | `string` | From quote/PSBT | | `swapId` | `string` | From PSBT step | | `address` | `string` | User BTC address | | `publicKey` | `string` | BTC pubkey | | `paymentAddress` | `string` | Change/receive address | | `paymentPublicKey` | `string` | Pubkey for change | | `protocol` | `string` | e.g., `runes` | | `signedPsbts` | `string[]` | Signed PSBT hex strings | ```ts theme={null} const result = await swaps.swapSubmit({ marketplace: quote.bestMarketplace, swapId: psbt.swapId, address: "bc1p...", publicKey: "...", paymentAddress: "...", paymentPublicKey: "...", protocol: "runes", signedPsbts: psbt.psbts.map(({ hex }) => sign(hex)), }); ``` **Response highlights** * `txid`: string * `marketplace`: string * `rbfProtection?`: `{ fundsPreparationTxId, fulfillmentId }` * `isRbfTxid`: boolean # V1 (legacy rune trading) These methods stay for existing integrations and RBF flows. * `signIn({ ord_address, btc_address, ord_public_key, btc_public_key, provider })` * `bind({ btcAddress, nftAddress, sign })` * `points({ ord_address })` * `popularCollections()` * `search({ rune_name, sell? })` * `fetchQuote({ btcAmount, runeName, address, marketplaces?, themeID?, sell?, rbfProtection?, fill? })` * `getPSBT({ orders, address, publicKey, paymentAddress, paymentPublicKey, runeName, utxos?, feeRate?, slippage?, themeID?, sell?, rbfProtection? })` * `confirmPSBT({ orders, address, publicKey, paymentAddress, paymentPublicKey, signedPsbtBase64, signedRbfPsbtBase64?, swapId, runeName, sell?, marketplaces?, rbfProtection? })` ## V1 flow (short) ```ts theme={null} const quote = await swaps.fetchQuote({ btcAmount: 0.0001, runeName: "LOBO•THE•WOLF•PUP", address: "bc1p...", marketplaces: ["MagicEden"], rbfProtection: true, }); const psbt = await swaps.getPSBT({ orders: quote.selectedOrders, address: "bc1p...", publicKey: "...", paymentAddress: "3Pc...", paymentPublicKey: "...", runeName: "LOBO•THE•WOLF•PUP", feeRate: 5, slippage: 9, rbfProtection: true, }); const confirmation = await swaps.confirmPSBT({ orders: quote.selectedOrders, address: "bc1p...", publicKey: "...", paymentAddress: "3Pc...", paymentPublicKey: "...", signedPsbtBase64: "", signedRbfPsbtBase64: psbt.rbfProtected?.base64 ? "" : undefined, swapId: psbt.swapId, runeName: "LOBO•THE•WOLF•PUP", rbfProtection: true, }); ``` # Errors All methods throw descriptive errors. Wrap calls in `try/catch` and surface useful hints (slippage, insufficient balance, marketplace unsupported, swap expired). For network issues, check connectivity and API key validity. # Examples Source: https://developer.satsterminal.com/swaps/examples End-to-end scripts for common V2 flows (buy, sell, alkanes, suite). ## Example 0 – Aggregator (swaps + borrow) ```ts theme={null} import { createClient } from "satsterminal-sdk"; import { ChainType } from "@satsterminal-sdk/borrow"; const { swaps, borrow } = createClient({ apiKey: process.env.API_KEY!, borrow: { chain: ChainType.BASE, wallet: /* your wallet */ }, }); const quote = await swaps.swapQuote({ amount: "0.001", fromToken: "BTC", toToken: "USDC", address: "bc1p...", protocol: "runes", params: {}, }); const setup = await borrow.setup(); console.log("Smart account:", setup.userStatus.smartAccountAddress); ``` ## Example 1 – Buy runes (BTC → rune) ```ts theme={null} import { SatsTerminal } from "satsterminal-sdk"; import readlineSync from "readline-sync"; const swaps = new SatsTerminal({ apiKey: process.env.API_KEY! }); const TRADE = { fromToken: "BTC", toToken: "LOBO•THE•WOLF•PUP", address: "bc1p...", publicKey: "...", paymentAddress: "3Pc...", paymentPublicKey: "...", amount: "0.0001", protocol: "runes", }; const quote = await swaps.swapQuote({ ...TRADE, params: {} }); const psbt = await swaps.swapPSBT({ ...TRADE, marketplace: quote.bestMarketplace, swapId: quote.swapId, feeRate: 5, slippage: 9, themeID: null, }); const signedPsbts = psbt.psbts.map(({ hex }, i) => { console.log(`PSBT ${i + 1}:`, hex.slice(0, 60), "..."); return readlineSync.question("Enter signed PSBT hex: "); }); const result = await swaps.swapSubmit({ ...TRADE, marketplace: quote.bestMarketplace, swapId: psbt.swapId, signedPsbts, }); console.log("✅ Swap complete:", result.txid); ``` ## Example 2 – Sell runes (rune → BTC) ```ts theme={null} const TRADE = { fromToken: "DOG•GO•TO•THE•MOON", toToken: "BTC", sellAmount: "10000", address: "bc1p...", publicKey: "...", paymentAddress: "3Pc...", paymentPublicKey: "...", protocol: "runes", }; const quote = await swaps.swapQuote({ amount: TRADE.sellAmount, fromToken: TRADE.fromToken, toToken: TRADE.toToken, address: TRADE.address, protocol: TRADE.protocol, params: {}, }); const psbt = await swaps.swapPSBT({ ...TRADE, marketplace: quote.bestMarketplace, swapId: quote.swapId, feeRate: 5, slippage: 9, themeID: null, }); const signedPsbts = psbt.psbts.map(({ hex }) => signWithWallet(hex)); const confirmation = await swaps.swapSubmit({ ...TRADE, marketplace: quote.bestMarketplace, swapId: psbt.swapId, signedPsbts, }); console.log("✅ Sold runes for BTC:", confirmation.txid); ``` ## Example 3 – Alkanes swap (BTC → token) ```ts theme={null} const TRADE = { fromToken: "BTC", toToken: "GOLD DUST", address: "bc1p...", publicKey: "...", paymentAddress: "bc1q...", paymentPublicKey: "...", amount: "0.00008", protocol: "alkanes", }; const quote = await swaps.swapQuote({ ...TRADE, params: {} }); if (quote.metrics) { Object.entries(quote.metrics).forEach(([m, metrics]) => { console.log(`${m}: ${metrics.percentFulfilled}% at ${metrics.averageUnitPrice}`); }); } const psbt = await swaps.swapPSBT({ ...TRADE, marketplace: quote.bestMarketplace, swapId: quote.swapId, feeRate: 3, slippage: 5, themeID: null, }); const signedPsbts = psbt.psbts.map(({ hex }) => signWithWallet(hex)); const result = await swaps.swapSubmit({ ...TRADE, marketplace: quote.bestMarketplace, swapId: psbt.swapId, signedPsbts, }); console.log("🎉 Alkanes swap complete:", result.txid); ``` ### Error-handling tips * Network issues: check API key + connectivity; retry with backoff. * Slippage/amount errors: increase `slippage` or reduce `amount`. * Marketplace errors: try another `marketplace` from the quote. * Swap expiration: re-run `swapQuote` to refresh the session. # Swaps Overview Source: https://developer.satsterminal.com/swaps/overview Understand the SatsTerminal swaps SDKs, V1 vs V2 flows, and when to use each. # Swaps at a glance * **V2 (recommended):** Simplified `swapQuote → swapPSBT → swapSubmit` flow with automatic marketplace selection and cleaner parameters. Supports multiple PSBTs per swap and protocols like `runes` and `alkanes`. * **V1 (legacy):** Granular rune trading with manual order handling (`fetchQuote → getPSBT → confirmPSBT`) and optional RBF protection. Kept for backwards compatibility. * **Packages:** Use the standalone `@satsterminal-sdk/swaps` or the suite `satsterminal-sdk` (exports the same `SatsTerminal` client). ## Install ```bash theme={null} npm install @satsterminal-sdk/swaps # or npm install satsterminal-sdk ``` ## Choose your flow | Use case | Go with | Why | | ----------------------------------- | -------------- | ----------------------------------------------------- | | New integrations, fastest path | **V2** | 3 calls, normalized data, auto marketplace routing | | RBF protection or legacy rune flows | **V1** | Explicit marketplace control and single-PSBT handling | | Multiple products (swaps + borrow) | **Aggregator** | One `createClient` call yields both clients | ## Protocols and tokens * `protocol`: `runes` for rune trading, `alkanes` for alkanes tokens, more coming. * `fromToken`/`toToken`: clear directionality (e.g., `BTC → USDC` or `DOG•GO•TO•THE•MOON → BTC`). * Marketplaces are selected automatically for V2; pass `marketplace` when creating PSBTs or submitting. ## Quick V2 shape ```ts theme={null} const quote = await swaps.swapQuote({ amount, fromToken, toToken, address, protocol, params: {} }); const psbt = await swaps.swapPSBT({ ...quote, address, publicKey, paymentAddress, paymentPublicKey, feeRate, slippage, protocol }); const result = await swaps.swapSubmit({ ...psbt, address, publicKey, paymentAddress, paymentPublicKey, protocol, signedPsbts }); ``` ## What changed from V1 * Direction is expressed via `fromToken`/`toToken` instead of `sell`. * Multiple PSBTs per swap (array) vs single PSBT. * Automatic order management; you only sign and submit. * Consistent response shapes and marketplace metrics. See the detailed API and examples: * **API reference:** `swaps/api` * **Quickstart:** `swaps/quickstart` * **Examples:** `swaps/examples` # Quickstart Source: https://developer.satsterminal.com/swaps/quickstart Ship a full V2 swap flow in minutes (quote → PSBT → submit). ## Install ```bash theme={null} npm install @satsterminal-sdk/swaps # or via suite npm install satsterminal-sdk ``` ## Initialize the client ```ts theme={null} import { SatsTerminal } from "@satsterminal-sdk/swaps"; // or: import { SatsTerminal } from "satsterminal-sdk"; const swaps = new SatsTerminal({ apiKey: process.env.API_KEY!, }); ``` ## 1) Get a V2 quote ```ts theme={null} const quote = await swaps.swapQuote({ amount: "0.001", fromToken: "BTC", toToken: "USDC", address: "bc1p...", protocol: "runes", params: {}, // keep empty unless protocol requires extras }); console.log(quote.bestMarketplace, quote.swapId, quote.metrics); ``` ## 2) Create PSBT(s) ```ts theme={null} const psbt = await swaps.swapPSBT({ marketplace: quote.bestMarketplace, swapId: quote.swapId, address: "bc1p...", publicKey: "", paymentAddress: "", paymentPublicKey: "", protocol: "runes", feeRate: 5, slippage: 9, themeID: null, // optional UI theme id }); // psbt.psbts is an array; each entry has { base64, hex, inputs } ``` ## 3) Sign + submit ```ts theme={null} const signedPsbts = psbt.psbts.map(({ hex }) => signWithWallet(hex)); const result = await swaps.swapSubmit({ marketplace: quote.bestMarketplace, swapId: psbt.swapId, address: "bc1p...", publicKey: "", paymentAddress: "", paymentPublicKey: "", protocol: "runes", signedPsbts, }); console.log("txid", result.txid, "marketplace", result.marketplace); ``` ## Optional: suite + borrow ```ts theme={null} import { createClient } from "satsterminal-sdk"; import { ChainType } from "@satsterminal-sdk/borrow"; const { swaps, borrow } = createClient({ apiKey: process.env.API_KEY!, borrow: { chain: ChainType.BASE, wallet: /* wallet provider */ }, }); ``` ## Legacy V1 flow (still supported) If you need explicit rune order handling or RBF protection, keep using: ```ts theme={null} await swaps.fetchQuote(); await swaps.getPSBT(); await swaps.confirmPSBT(); ``` # Complete Borrow App Source: https://developer.satsterminal.com/ui-components/borrow-app Install a ready-made single-page borrowing experience with one registry command. # Complete Borrow App `BorrowApp` composes the complete SatsTerminal borrowing journey into one framework-agnostic application. It includes loan creation, offer selection, workflow tracking, loan history, focused management views, repayment, collateral withdrawal, and platform-wallet EVM withdrawal. No application routes are required. Selecting, tracking, or creating a loan opens a focused view controlled by URL search parameters. ## Install ```bash theme={null} npx shadcn@latest add https://ui.satsterminal.com/r/borrow-app.json ``` This single command installs every supporting SatsTerminal block and required shadcn primitive automatically. `BorrowApp` requires `@satsterminal-sdk/borrow` 1.7.3 or later. ## Render ```tsx theme={null} "use client"; import { BorrowApp } from "@/components/borrow-app"; export function BorrowPage() { return ; } ``` `BorrowApp` includes a minimal UniSat connector, owns the `BorrowProvider`, restores the extension account and SDK session, and starts a fresh SDK session only when restoration is unavailable. No wallet package is required. Applications that already manage a wallet can pass their own SDK-compatible `wallet` prop; doing so bypasses the built-in UniSat connection screen. ## Included flow The installed app composes: * `LoanComposer` and `AvailableOffers` * Resumable `BorrowFlow` * `UserLoans` * `LoanManagement` for Deposit more and Borrow more * `RepayLoan` * `WithdrawCollateral` * `WalletWithdrawal` for EVM platform-wallet transfers * `PlatformWalletAddress` in the application header ## Focused loan pages `BorrowApp` starts on a dashboard containing the composer, offers, history, and wallet withdrawal. Creating a loan or selecting an existing loan switches to a focused transaction view instead of stacking management components below the dashboard. The selected view is stored in namespaced `stBorrow*` URL search parameters using the browser History API. Refreshing preserves the current transaction, browser Back returns to the dashboard, and the component remains framework-agnostic—no Next.js or React Router dependency is required. ## Props | Prop | Type | Purpose | | ----------------------- | --------------------------------------------- | -------------------------------------------------------------------------- | | `apiKey` | `string` | Required SatsTerminal API key unless `sdkOverride` is supplied | | `wallet` | `WalletProvider` | Optional external Bitcoin adapter; bypasses built-in UniSat connection | | `sdkConfig` | `Omit` | Optional overrides for the SDK configuration | | `sdkOverride` | `BorrowSDK` | Alternative pre-built SDK instance for tests or advanced integrations | | `destinationAddress` | `string` | Optional payout override passed to loan execution | | `defaultBitcoinAddress` | `string` | Default destination for collateral withdrawal | | `defaultEvmAddress` | `string` | Default destination for platform-wallet withdrawal | | `autoRestore` | `boolean` | Restore persisted SDK sessions; defaults to `true` | | `autoStart` | `boolean` | Initialize a session when restoration fails; defaults to `true` | | `title` | `string` | Application header title | | `accountLabel` | `string` | Connected-wallet label shown in the header | | `onDisconnect` | `() => void` | Optional callback after built-in disconnect, or external disconnect action | | `onLoanSelected` | `(loan) => void` | Observe a loan being opened from history | ## Atomic components Use the individual registry blocks instead when you need custom routing or a different layout. `BorrowApp` installs the same editable source components; it does not introduce a separate closed component system. # Borrow UI Quickstart Source: https://developer.satsterminal.com/ui-components/borrow-quickstart Compose an end-to-end borrowing interface from SatsTerminal registry components. # Borrow UI quickstart Want to see this custom-composition approach in a complete application? The official [Borrow SDK Example](https://github.com/Sats-Terminal/borrow-sdk-example/tree/main) integrates the SDK with the individual registry components shown below. It does not use the all-in-one `BorrowApp`. ## Recommended: complete flow ```bash theme={null} npx shadcn@latest add https://ui.satsterminal.com/r/borrow-app.json ``` ```tsx theme={null} import { BorrowApp } from "@/components/borrow-app"; ``` This provides built-in UniSat connection plus the complete borrowing and loan-management experience. Pass an external `wallet` only when your application already manages wallet connectivity. Continue below only when you want to compose the atomic components into a custom layout. ## Custom composition ### Install the flow ```bash theme={null} npx shadcn@latest add https://ui.satsterminal.com/r/platform-wallet-address.json npx shadcn@latest add https://ui.satsterminal.com/r/loan-composer.json npx shadcn@latest add https://ui.satsterminal.com/r/available-offers.json npx shadcn@latest add https://ui.satsterminal.com/r/borrow-flow.json npx shadcn@latest add https://ui.satsterminal.com/r/user-loans.json npx shadcn@latest add https://ui.satsterminal.com/r/wallet-withdrawal.json ``` Registry dependencies such as `borrow-provider`, `borrow-core`, and the required shadcn primitives are installed automatically. ### Compose the page ```tsx theme={null} "use client"; import { ActiveBorrowFlow } from "@/components/active-borrow-flow"; import { AvailableOffers } from "@/components/available-offers"; import { BorrowProvider } from "@/components/borrow-context"; import { LoanComposer } from "@/components/loan-composer"; import { PlatformWalletAddress } from "@/components/platform-wallet-address"; import { UserLoans } from "@/components/user-loans"; import { WalletWithdrawal } from "@/components/wallet-withdrawal"; export function BorrowPage() { return (

openLoan(loan.originalBorrowId)} onTrackLoan={(loan) => openWorkflow(loan.workflowId ?? loan.originalBorrowId) } />
); } ``` `walletProvider`, `openLoan`, and `openWorkflow` are application integrations. Connect them to your wallet adapter and router. ### What happens 1. `BorrowProvider` restores or starts the SDK session. 2. `LoanComposer` calculates collateral and requests quotes. 3. `AvailableOffers` executes the selected quote. 4. `ActiveBorrowFlow` shows the Bitcoin deposit and workflow status. 5. `UserLoans` loads active, pending, and historical loans for the connected wallet. 6. `WalletWithdrawal` moves supported platform-wallet assets to an EVM address. For repayment and collateral actions, install the management components described in the [Component reference](/ui-components/component-reference). # UI Component Reference Source: https://developer.satsterminal.com/ui-components/component-reference Available SatsTerminal registry components, installation commands, and SDK responsibilities. # Component reference Install any public component with: ```bash theme={null} npx shadcn@latest add https://ui.satsterminal.com/r/.json ``` ## Complete application | Component | Purpose | Important props | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | `borrow-app` | Complete app with built-in UniSat connection, creation, tracking, history, management, repayment, collateral, and wallet withdrawal | `apiKey`; optional `wallet`, `defaultBitcoinAddress`, `defaultEvmAddress` | See [Complete Borrow App](/ui-components/borrow-app) for the one-command installation path. ## Foundation | Component | Purpose | Main installed files | | ----------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `borrow-core` | Shared reducer state, workflow events, BTC pricing, calculations, and formatters | `lib/borrow-events.ts`, `lib/provider-reducer.ts`, `lib/btc-price.ts` and related utilities | | `borrow-provider` | Creates the `BorrowSDK`, restores sessions, and exposes state and actions | `components/borrow-context.tsx`, `hooks/use-borrow-actions.ts` | Most applications install `borrow-provider`; other registry items install `borrow-core` transitively when needed. ## Origination | Component | Purpose | Important props | | ------------------ | ------------------------------------------------------------------- | -------------------------------------------------------- | | `loan-composer` | Borrow amount, LTV slider, live BTC collateral, and quote request | `hideAfterExecution`, `className` | | `available-offers` | Protocol offers with APY, chain, LTV, and click-to-execute behavior | `destinationAddress`, `loadingPlaceholderCount` | | `borrow-flow` | Deposit QR, mempool link, workflow timeline, and completion state | `loanId`, `startedAt`, `depositWindowSeconds`, `onReset` | Installing `borrow-flow` also provides `ActiveBorrowFlow`, which reads the current workflow ID from `BorrowProvider` automatically. ## Account and loan history | Component | Purpose | Important props | | ------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------- | | `platform-wallet-address` | Compact platform wallet address and copy action | `label`, `className` | | `user-loans` | Active, pending, and historical loans with pagination | `defaultFilter`, `pageSize`, `onManageLoan`, `onTrackLoan` | | `wallet-withdrawal` | Withdraw supported platform-wallet assets to an EVM address | `defaultEvmAddress`, `pollIntervalMs`, `onComplete` | See [Platform wallet withdrawal](/ui-components/wallet-withdrawal) for destination behavior and status tracking. ## Loan management | Component | Purpose | Important props | | --------------------- | ----------------------------------------------------------- | ------------------------------------------------------------ | | `loan-management` | Deposit more BTC or borrow more against an active loan | `defaultLoanId`, `className` | | `repay-loan` | Full or partial repayment with live debt and wallet balance | `loanId`, `currency`, `chain`, `defaultAmount`, `onComplete` | | `withdraw-collateral` | Withdraw BTC using the live maximum and health limits | `loanId`, `defaultBitcoinAddress`, `onComplete` | Install an individual management action: ```bash theme={null} npx shadcn@latest add https://ui.satsterminal.com/r/loan-management.json npx shadcn@latest add https://ui.satsterminal.com/r/repay-loan.json npx shadcn@latest add https://ui.satsterminal.com/r/withdraw-collateral.json ``` `loan-management` contains both Deposit More and Borrow More modes. They are not separate registry items. Its deposit transaction link and confirmation progress require `@satsterminal-sdk/borrow` 1.7.3 or later. ```tsx theme={null} ``` All components that call the SDK must be descendants of `BorrowProvider`. # Customize UI Components Source: https://developer.satsterminal.com/ui-components/customization Safely adapt installed SatsTerminal components to your product and design system. # Customization Registry components become part of your application after installation. You can change their markup, styling, copy, routing, and composition without forking an npm package. ## What to customize * Tailwind classes and responsive layout * Headings, helper text, empty states, and loading states * Routing in `onManageLoan` and `onTrackLoan` * Wallet connection and disconnection controls * Placement of the platform wallet address * Success callbacks and application notifications ## Keep SDK operations centralized Use `BorrowProvider` as the owner of the `BorrowSDK` instance. Components can access it through the installed context: ```tsx theme={null} import { useBorrowContext } from "@/components/borrow-context"; export function SessionButton() { const { state, actions } = useBorrowContext(); return ( ); } ``` Keeping one provider prevents multiple SDK instances from starting overlapping sessions or wallet-signature requests. ## Preserve workflow safeguards When changing transaction components, retain: * Loading and duplicate-submission guards * Live loan-position refreshes * Minimum amount and maximum-withdrawable validation * Fully repaid loan checks * Workflow completion and error handling ## Updating an installed component Registry installation copies source code; it does not automatically update existing files when the registry changes. Review the new registry source before running the add command again, especially when you have local customizations. Use version control to compare and merge upstream changes instead of overwriting customized files blindly. # Install UI Components Source: https://developer.satsterminal.com/ui-components/installation Configure shadcn and install components from the SatsTerminal UI registry. # Installation ## Requirements * React 19 or a compatible React framework * Tailwind CSS * A shadcn-compatible `components.json` * `@satsterminal-sdk/borrow` 1.7.3 or later * A SatsTerminal API key and Bitcoin wallet adapter ## Initialize shadcn If your project does not already have a `components.json`, initialize shadcn: ```bash theme={null} npx shadcn@latest init ``` ## Install the provider Every SatsTerminal Borrow component reads the SDK instance and shared state from `BorrowProvider`: ```bash theme={null} npx shadcn@latest add https://ui.satsterminal.com/r/borrow-provider.json ``` The CLI also installs the provider's registry and npm dependencies. ## Install a component Install components individually using their registry URL: ```bash theme={null} npx shadcn@latest add https://ui.satsterminal.com/r/loan-composer.json npx shadcn@latest add https://ui.satsterminal.com/r/available-offers.json npx shadcn@latest add https://ui.satsterminal.com/r/borrow-flow.json ``` You do not need to install `borrow-core` separately when a selected component declares it as a dependency. ## Configure the provider Wrap the borrowing interface with the installed provider: ```tsx theme={null} "use client"; import { BorrowProvider } from "@/components/borrow-context"; export function BorrowPage() { return ( {/* Installed SatsTerminal components */} ); } ``` `autoRestore` silently restores a valid persisted session. When no session can be restored, `autoStart` initializes one and may request a Bitcoin wallet signature. Your wallet adapter must satisfy the SDK's `WalletProvider` interface. See [React integration](/borrow/examples/react-integration) for wallet integration patterns. ## Installed folders The exact aliases come from your `components.json`. With the standard configuration, registry files are copied into: * `components/` for visual components and the provider * `hooks/` for data and workflow hooks * `lib/` for reducers, types, calculations, and formatters * `components/ui/` for required shadcn primitives Continue to the [Borrow quickstart](/ui-components/borrow-quickstart) to compose the installed components. # UI Components Source: https://developer.satsterminal.com/ui-components/overview Editable React components for building SatsTerminal SDK experiences. # SatsTerminal UI Components The SatsTerminal UI registry provides ready-made React components powered by the SatsTerminal SDK. Install only the parts you need, then customize the source inside your application. Unlike a closed component package, the registry uses the shadcn CLI to copy component, hook, and utility files into your project. You own the installed code while the SDK continues to provide quotes, sessions, workflows, loan history, repayment, and collateral operations. ## Registry and documentation | Surface | Purpose | | ------------------------------------------ | ------------------------------------------------------ | | `ui.satsterminal.com` | Registry endpoint used by the CLI | | `developer.satsterminal.com/ui-components` | Installation, integration, and component documentation | | `@satsterminal-sdk/borrow` | Borrowing logic and API methods used by the components | ## What is included * Loan amount and LTV composition * Available protocol offers * Borrow workflow and Bitcoin deposit tracking * Platform wallet address * Active, pending, and historical loans * Deposit more and borrow more actions * Full and partial repayment * Collateral withdrawal with live safety limits * Platform-wallet asset withdrawal to EVM addresses ## Start here 1. Install the ready-made [Complete Borrow App](/ui-components/borrow-app). 2. Follow [Installation](/ui-components/installation) for registry setup details. 3. Build a custom composition with the [Borrow quickstart](/ui-components/borrow-quickstart). 4. Browse the [Component reference](/ui-components/component-reference). ## Reference implementation The official [Borrow SDK Example](https://github.com/Sats-Terminal/borrow-sdk-example/tree/main) is a complete Next.js application that integrates the Borrow SDK using the individual registry components. It demonstrates wallet connection, provider and session setup, borrowing workflows, loan history, routed loan management, repayment, collateral actions, and wallet withdrawals. The repository intentionally uses the atomic components—not the all-in-one `BorrowApp`—so it is the best starting point when you want to study the SDK integration or build a custom application structure. Use `BorrowApp` when you prefer the ready-made, framework-agnostic flow. The registry currently contains Borrow components. The same registry can add Swaps, Bridge, and Earn components without changing the installation model. # Platform Wallet Withdrawal Source: https://developer.satsterminal.com/ui-components/wallet-withdrawal Withdraw supported assets from the SatsTerminal platform wallet to an EVM address. # Platform wallet withdrawal `WalletWithdrawal` is an atomic, loan-independent component for moving assets held by the user's platform wallet. It uses live wallet positions instead of asking the application to supply asset balances. This is different from `WithdrawCollateral`, which removes collateral from a specific loan and is constrained by that loan's health factor. ## Install ```bash theme={null} npx shadcn@latest add https://ui.satsterminal.com/r/wallet-withdrawal.json ``` The registry installs `BorrowProvider` and the required shadcn primitives when they are not already present. `WalletWithdrawal` requires `@satsterminal-sdk/borrow` 1.7.2 or later. Earlier versions do not return the asynchronous EVM withdrawal transaction ID correctly. ## Render ```tsx theme={null} import { WalletWithdrawal } from "@/components/wallet-withdrawal"; { console.log("Withdrawal created", transactionId); }} /> ``` ## SDK methods | Method | Responsibility | | ---------------------- | ------------------------------------------------------------ | | `getWalletPositions()` | Loads platform-wallet assets, balances, symbols, and chains | | `withdrawToEVM()` | Creates a sponsored same-chain transfer to an EVM address | | `getWithdrawStatus()` | Tracks the withdrawal and exposes the final transaction hash | ## EVM destination * Transfers the selected asset on its existing source chain. * Validates the destination as an EVM address. * Uses the platform wallet at index 0. * Gas is sponsored by the platform. ## Balance and status behavior The component prevents zero, negative, and over-balance submissions. The **Max** action uses the live position balance. After submission, it polls the asynchronous transaction, exposes an explorer link when the final hash is available, and refreshes wallet positions on completion. ```tsx theme={null} { console.log(progress.stage, progress.transactionHash); }} /> ``` An asset visible in the platform wallet is selectable only when sponsored EVM transfer is configured for that asset and chain.