@blend-money/sdk is the public SDK package for Blend neobank integrations. It exports one class, BlendClient, with flat domain namespaces:
client.safe
client.balance
client.positions
client.returns
client.yield
client.rebalance
client.deposit
client.withdraw
Install
pnpm add @blend-money/sdk
Initialize
import { BlendClient } from "@blend-money/sdk";
import { http } from "viem";
const client = new BlendClient({
baseUrl: "https://api.portal.blend.money",
apiKey: "blend_xxx",
neobankId: "acme-bank",
accountTypeId: "uuid-xxx",
transports: {
8453: http("https://base-mainnet.g.alchemy.com/v2/MY_KEY"),
},
paymasterTransport: http("https://api.pimlico.io/v2/8453/rpc?apikey=MY_KEY"),
});
Create one client per neobank plus account type. Per-user values like the EOA, accountId, and Safe address are passed as method arguments, never stored in config.
Configuration fields
| Field | Type | Required | Description |
|---|
baseUrl | string | Yes | Blend API base URL (e.g. https://api.portal.blend.money) |
apiKey | string | Yes | Sent as X-API-Key header on every request |
neobankId | string | Yes | Your neobank slug — used in the /extern/:neobankId/ path segment |
accountTypeId | string | Yes | Account type UUID — used in /extern/:neobankId/:accountTypeId/ |
transports | Record<number, Transport> | Yes | Viem transports keyed by chain ID — your RPC infrastructure |
paymasterTransport | Transport | Yes | Viem transport for the paymaster / bundler (e.g. Pimlico) |
timeoutMs | number | No | Request timeout in milliseconds (default: 15000) |
retries | number | No | Max retry attempts for retryable errors (default: 3) |
The SDK automatically retries on 429, 500, 502, 503, and 504 responses with exponential backoff up to retries attempts. You do not need to implement retry logic on top of the client.
Account and Safe APIs
client.safe.account(address)
Fetches or creates the Blend account for a user EOA. This is the usual entry point for a user session.
Returns SafeAccountResponse:
| Field | Type | Description |
|---|
accountId | string | Blend account UUID — required by balance, positions, returns, and deposit APIs |
safeAddress | Hex | Deterministic Safe wallet address |
chainsDeployed | number[] | Chain IDs where the Safe is already deployed |
const account = await client.safe.account(userEoa);
// account.accountId
// account.safeAddress
// account.chainsDeployed
client.safe.resolve(accountId, chainId)
Checks Safe deployment state for a specific chain. Returns a discriminated union keyed on status:
| Status | Meaning |
|---|
"validated" | Safe is deployed and live — includes accountId, userAddress, safeAddress, chainId |
"not-deployed" | Safe address is known but not yet deployed on this chain |
"invalid" | Safe address does not match expected derivation |
"disconnected" | Safe exists but the role-modifier module is not enabled |
const resolved = await client.safe.resolve(account.accountId, 8453);
if (resolved.status === "validated") {
// resolved.accountId, resolved.safeAddress, resolved.chainId
}
client.safe.request(accountId, chainId)
Requests Safe deployment on a specific chain.
await client.safe.request(account.accountId, 8453);
Account-scoped reads
All reads in this section (balances, positions, returns, yield) are routed through the Blend backend API. The client does not need an RPC connection, a viem provider, or a connected wallet to fetch these state values.
client.balance.get(accountId)
const balance = await client.balance.get(account.accountId);
client.balance.getHistory(accountId, params?)
const history = await client.balance.getHistory(account.accountId, {
startDate: "2026-03-01",
endDate: "2026-03-09",
});
client.positions.get(accountId)
const positions = await client.positions.get(account.accountId);
client.returns.get(accountId)
const returns = await client.returns.get(account.accountId);
client.yield.get()
const yieldInfo = await client.yield.get();
Deposit APIs
client.deposit.getTokens(chainId?, address?)
Returns a token catalog with chains and tokens arrays.
const catalog = await client.deposit.getTokens(8453);
client.deposit.getBalances(eoa, chainId)
Returns non-zero ERC-20 balances for the user EOA on the source chain.
Like account-scoped reads, this query is handled by the Blend backend. It does not require an RPC connection or wallet provider on the client.
const balances = await client.deposit.getBalances(userEoa, 8453);
client.deposit.getQuote(params)
Requests a deposit quote. accountId is required because Blend resolves the destination Safe and account-type configuration from it.
DepositQuoteParams:
| Field | Type | Description |
|---|
chainId | number | Origin chain the user is depositing from |
inputAssetAddress | string | Token contract address on the origin chain |
eoa | string | User’s EOA address |
accountId | string | Blend account UUID (from safe.account()) |
amount | string | Amount in the input token’s smallest units |
const account = await client.safe.account(userEoa);
const quote = await client.deposit.getQuote({
chainId: 8453,
inputAssetAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
eoa: userEoa,
accountId: account.accountId,
amount: "1000000",
});
Withdraw API
client.withdraw.getCalldata(params)
Returns server-built withdrawal execution payloads. The server resolves the Safe and user context from the accountId, selects source chains, and returns an ordered payload set for the integrator to execute.
const result = await client.withdraw.getCalldata({
accountId: account.accountId,
destinationChainId: 8453,
amount: "1000000000000000000",
// isMaxWithdraw: true,
});
for (const payload of result.payloads) {
if (payload.liquidityReset) {
// submit delegatecall first
}
for (const txn of payload.withdraw) {
// then submit each withdraw transaction in order
}
if (payload.bridge) {
// finally execute the bridge quote when present
}
}
Each WithdrawCalldataPayload contains:
| Field | Type | Description |
|---|
chainId | number | Source chain for this payload |
vaultAddress | string | ERC-4626 vault being exited |
amount | string | Amount allocated from this chain |
liquidityReset | { target, data } | null | Rebalance calldata to flush positions — submit as delegatecall |
withdraw | Array<{ target, data }> | Ordered ERC-4626 withdraw transactions (approve, multicall, approve-reset) |
bridge | unknown | null | Relay bridge quote when source chain differs from destinationChainId |
Important behavior:
amount is an integer string in underlying token units
isMaxWithdraw: true redeems all shares across chains
- a
409 response means a rebalance flow plan is already active
- for max withdraws, bridge payloads may be omitted because the exact post-settlement amount is not known up front
Rebalance APIs
client.rebalance.getCandidates()
const { candidates } = await client.rebalance.getCandidates();
client.rebalance.createRequest(requestData?)
Triggers a scheduler-driven rebalance request. Returns { requestId: string | null, candidateCount: number }.
requestId is null when there are no open candidates. Always check before polling with getRequest().
const request = await client.rebalance.createRequest();
if (request.requestId) {
const status = await client.rebalance.getRequest(request.requestId);
}
client.rebalance.getRequest(id)
const status = await client.rebalance.getRequest(request.requestId);
Error handling
import { SdkError } from "@blend-money/sdk";
try {
const account = await client.safe.account(userEoa);
} catch (error) {
const sdkError =
error instanceof SdkError ? error : SdkError.fromAxiosError(error);
if (sdkError.isRetryable()) {
// retry with backoff
} else {
console.error(sdkError.getUserMessage());
}
}
Types
All major config, domain, and integration types are exported directly from @blend-money/sdk, including:
BlendClientConfig
SafeAccountResponse
BalanceResponse
PositionsResponse
ReturnsResponse
YieldResponse
CandidatesResult
RebalanceRequest
TokenCatalog
DepositToken
DepositBalance
DepositQuoteParams
DepositQuoteResponse
WithdrawCalldataParams
WithdrawCalldataPayload
WithdrawCalldataResult
SdkError
Advanced utilities
The SDK still exports advanced Safe utilities such as TransactionHandler, SafeMultisendManager, combineActionPlans, ActionPlan, and Txn.
These are advanced building blocks. The primary public withdrawal path is client.withdraw.getCalldata(), not client-built action plans.