@blend-money/sdk
v2.4.0
Published
Blend SDK for B2B neobank integrations.
Downloads
922
Readme
Blend SDK
TypeScript SDK for the Blend Protocol. Frontend-first, wallet-authenticated via SIWE.
Install
pnpm add @blend-money/sdkPeer dependencies: viem
Quick Start
import { BlendSdk } from "@blend-money/sdk";
const sdk = new BlendSdk({
publishableKey: "pk_live_...",
signMessage: walletClient.signMessage,
paymasterRpcUrl: "https://your-bundler.example.com/v2/8453/rpc?apikey=...",
});
// 1. Discover what's available
const chains = await sdk.discover.depositChains();
const tokens = await sdk.discover.depositTokens(8453);
// 2. Sign in with SIWE
const session = await sdk.signIn({ address: "0x...", chainId: 8453 });
// 3. Get a quote
const quote = await sdk.quoteDeposit({
chainId: 8453,
tokenAddress: "0xA0b86991...",
amount: "1000000",
});
// 4. Show confirmation UI
console.log(quote.input); // { symbol: "USDC", amount: "1000000", amountUsd: "1" }
console.log(quote.output); // { symbol: "USDC", amount: "1000000", amountUsd: "1" }
console.log(quote.fees); // { totalUsd: "0.01" }
// 5. Execute
const result = await sdk.execute(quote, {
signerAddress: address,
deriveSigner: async (chainId) => ({
signer: getWalletClient({ chainId }),
publicClient: getPublicClient({ chainId }),
}),
onStatusChange: (status) => console.log(status),
});
if (result.status === "settled") {
console.log("Done!", result.txHashes);
}Configuration
const sdk = new BlendSdk({
// Required
publishableKey: "pk_live_...", // Safe for client-side code
signMessage: (msg) => wallet.sign(msg), // SIWE signature function
// Required for execute()
paymasterRpcUrl: "https://...", // Any ERC-4337 bundler/paymaster
// Optional
baseUrl: "https://...", // API base URL override
fiatCurrency: "EUR", // ISO 4217 code for multi-currency
timeoutMs: 15000, // Request timeout (default: 15000)
retries: 3, // Max retries for 429/5xx (default: 3)
});The paymasterRpcUrl is optional for read-only usage (discovery, account data) but required when calling sdk.execute().
API Reference
Authentication
// Sign in — wallet prompted to sign a SIWE message
const session = await sdk.signIn({ address: "0x...", chainId: 8453 });
// session: { address, accountId, safeAddress, chainsDeployed, expiresAt }
// Sign out
await sdk.signOut();
// Check state
sdk.isSignedIn; // boolean
sdk.session; // AuthSession (throws if not signed in)Session Persistence
Sessions are in-memory by default and lost on page reload. Use exportSession() and restoreSession() to persist across reloads. Expired tokens are automatically refreshed on the next API call. Use sessionStorage (cleared when the tab closes) unless you need cross-tab persistence — localStorage extends XSS exposure to the token's full lifetime.
// After sign-in, save the session
await sdk.signIn({ address: "0x...", chainId: 8453 });
sessionStorage.setItem("blend-session", JSON.stringify(sdk.exportSession()));
// On page load, restore the session
const stored = sessionStorage.getItem("blend-session");
if (stored) {
try {
sdk.restoreSession(JSON.parse(stored));
// sdk.isSignedIn === true — no wallet prompt needed
} catch {
// Corrupt data — fall back to signIn
sessionStorage.removeItem("blend-session");
}
}Discovery
No authentication required. When signed in, depositTokens automatically filters to tokens the wallet holds.
const chains = await sdk.discover.depositChains();
// [{ chainId, name, displayName, iconUrl }]
const tokens = await sdk.discover.depositTokens(8453);
// [{ chainId, address, symbol, name, decimals, logoURI, price, balance?, amount? }]
const destinations = await sdk.discover.withdrawDestinations();
// [{ chainId, name, loanTokenAddress }]
const yieldData = await sdk.discover.yield();
// { accountTypeId, yieldBreakdown: [{ chainId, vaultAddress, breakdown, summary }] }Amount Helpers
The SDK requires amounts in the token's smallest unit (e.g. "1000000" for 1 USDC). Use parseAmount and formatAmount to convert between human-readable and smallest-unit strings without floating-point precision loss.
import { parseAmount, formatAmount } from "@blend-money/sdk";
// Human-readable → smallest unit
const smallest = parseAmount("100.50", 6); // "100500000" (USDC has 6 decimals)
// Smallest unit → human-readable
const human = formatAmount("100500000", 6); // "100.5"
// Use with discovered tokens
const tokens = await sdk.discover.depositTokens(8453);
const usdc = tokens.find((t) => t.symbol === "USDC")!;
const amount = parseAmount(userInput, usdc.decimals);
const quote = await sdk.quoteDeposit({
chainId: 8453,
tokenAddress: usdc.address,
amount,
});Quoting
Requires authentication. Creates or reuses a session, then quotes it. Returns a clean object for your confirmation UI.
// Deposit quote
const quote = await sdk.quoteDeposit({
chainId: 8453,
tokenAddress: "0xA0b86991...",
amount: "1000000",
externalRef: "order-123", // optional — for reconciliation
forceReset: false, // optional — cancel existing session first (default: false)
});
// DepositQuote: { type, intentId, originChainId, destinationChainId,
// input, output, fees, estimatedSeconds, expiresAt }
// Withdrawal quote
const quote = await sdk.quoteWithdraw({
destinationChainId: 8453,
amount: "1000000",
isMaxWithdraw: false, // optional — redeem all shares (default: false)
forceReset: false, // optional
});
// WithdrawQuote: { type, intentId, destinationChainId, totalAmount,
// totalFeesUsd, estimatedSeconds, sourceChainCount, expiresAt }Live Quoting
Calling quoteDeposit or quoteWithdraw multiple times re-quotes the same open session with the new parameters. No need for forceReset just to change the amount or token.
// User types $1
const quote1 = await sdk.quoteDeposit({
chainId: 8453,
tokenAddress: "0xA0b86991...",
amount: parseAmount("1", 6),
});
// User changes to $5 — same session, fresh quote
const quote2 = await sdk.quoteDeposit({
chainId: 8453,
tokenAddress: "0xA0b86991...",
amount: parseAmount("5", 6),
});
// quote2 reflects the updated amount — same intentId, fresh prices
// Switching from withdraw to deposit also works on open sessions
const depositQuote = await sdk.quoteDeposit({ ... });Execution
Pass a quote, a signer address, and a deriveSigner callback that returns viem clients for a given chain. The SDK handles session locking, on-chain submission (Safe UserOps via the paymaster), hash submission, and settlement polling internally. For multi-chain withdrawals, deriveSigner is called once per source chain.
const result = await sdk.execute(quote, {
signerAddress: address, // EOA address (Hex)
deriveSigner: async (chainId) => ({
// returns viem clients for the chain
signer: getWalletClient({ chainId }),
publicClient: getPublicClient({ chainId }),
}),
isContractSigner: false, // optional — for smart contract wallets (e.g. Coinbase)
onStatusChange: (status) => {
// optional — "executing" | "confirming" | "settled" | "failed"
updateUI(status);
},
pollIntervalMs: 3000, // optional (default: 3000)
pollTimeoutMs: 300000, // optional (default: 300000)
});
// ExecuteResult: { status, txHashes, settledAt, error }Account Data
Requires authentication.
const balance = await sdk.account.balance();
// { accountId, safeAddress, perChain, total }
const history = await sdk.account.balanceHistory({ startDate: "2025-01-01" });
// [{ id, createdAt, totalValue, totalYield, breakdown }]
const positions = await sdk.account.positions();
// { accountId, safeAddress, events }
const returns = await sdk.account.returns();
// { current, totalDeposited, totalWithdrawn, netDeposited, returns, returnsPct }Safe Management
// Resolve Safe deployment on a chain (requires auth)
const resolution = await sdk.account.safe.resolve(8453);
// { status: "validated", accountId, userAddress, safeAddress, chainId }
// Request Safe deployment (requires auth)
await sdk.account.safe.request(8453);
// Look up account by EOA (no auth required)
const account = await sdk.account.safe.lookup("0x...");
// { accountId, safeAddress, chainsDeployed }Power-User: Session Lifecycle
For advanced use cases, the full intent session lifecycle is available via sdk.sessions:
const session = await sdk.sessions.createSession({ forceReset: true });
const quoted = await sdk.sessions.quoteDeposit(session.intentId, { ... });
const locked = await sdk.sessions.lock(session.intentId, { signerAddress: "0x..." });
const submitted = await sdk.sessions.submit(session.intentId, { txHashes: [...] });
const result = await sdk.sessions.execute(session.intentId, { ... });
const sessions = await sdk.sessions.list();
await sdk.sessions.cancel(session.intentId);Error Handling
import { SdkError, SESSION_ERROR_CODES } from "@blend-money/sdk";
try {
await sdk.quoteDeposit({ ... });
} catch (err) {
if (err instanceof SdkError) {
console.log(err.code); // e.g. "AUTH_NOT_SIGNED_IN"
console.log(err.status); // HTTP status
console.log(err.getUserMessage()); // human-readable message
console.log(err.isRetryable()); // true for 429, 5xx, network errors
}
}Error codes: AUTH_NOT_SIGNED_IN, AUTH_CHALLENGE_FAILED, AUTH_VERIFICATION_FAILED, AUTH_TOKEN_EXPIRED, AUTH_INVALID_SESSION, INTENT_NOT_FOUND, INTENT_EXPIRED, INTENT_WRONG_STATUS, INTENT_CONCURRENT_TRANSITION, SESSION_NOT_QUOTED, SESSION_LOCKED_BY_OTHER, SETTLEMENT_TIMEOUT, FLOWPLAN_CONFLICT.
Development
pnpm install # Install dependencies
pnpm build # Build
pnpm dev # Watch mode
pnpm test # Run tests (interactive)
pnpm test:run # Run tests (CI)
pnpm test:coverage # Coverage report
pnpm check-types # Type check
pnpm format # PrettierLicense
MIT
