> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blend.money/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK Reference

> Authentication, account management, balance queries, error handling, and TypeScript type definitions.

Authentication, account management, balance queries, error handling, and TypeScript type definitions.

## Authentication

### SIWE flow (frontend)

The frontend SDK uses Sign-In with Ethereum. The user's wallet signs a challenge message. Blend returns a JWT that binds the wallet to a Blend account.

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
    participant Wallet as User's Wallet
    participant App as Your App
    participant SDK as BlendSdk
    participant API as Blend API

    App->>SDK: sdk.signIn({ address, chainId })
    SDK->>API: POST /auth/challenge
    API-->>SDK: SIWE message + nonce
    SDK->>Wallet: Sign this message
    Wallet-->>SDK: Signature
    SDK->>API: POST /auth/verify
    API-->>SDK: JWT (1-hour expiry)
    SDK-->>App: AuthSession
```

The JWT contains the wallet address, account ID, and account type. Every SDK call after sign-in includes it automatically. You never pass an account ID.

### API key auth (server)

The server SDK authenticates with an API key (`sk_live_`) in the `X-API-Key` header. No challenge/verify step. The API key replaces both the publishable key and the SIWE session.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { BlendServerSdk } from "@blend-money/node";

const sdk = new BlendServerSdk({
  apiKey: process.env.BLEND_API_KEY!,
  accountTypeId: "savings-usd",
});
```

You pass `accountId` explicitly to every account-scoped call via `sdk.forAccount(accountId)`.

### Rate limits

| Endpoint                                | Limit            | Scoped by      |
| --------------------------------------- | ---------------- | -------------- |
| SIWE challenge (`POST /auth/challenge`) | 10 / 60 seconds  | IP address     |
| SIWE verify (`POST /auth/verify`)       | 5 / 60 seconds   | IP address     |
| Account-scoped routes (frontend)        | 100 / 60 seconds | Wallet address |
| Server routes before authentication     | 100 / 60 seconds | IP address     |
| Account-scoped routes (server)          | 100 / 60 seconds | API key        |

Exceeding limits returns a `429` with error code `RATE_LIMITED` or `RATE_LIMIT`.

### Session security

* Keep JWTs in memory. Use `sessionStorage` for persistence across reloads. Avoid `localStorage`.
* Clear the session when the wallet disconnects or address changes.
* Never share an exported session across users or account types.

### Auth error codes

| Code                       | Status | What to do                                                               |
| -------------------------- | ------ | ------------------------------------------------------------------------ |
| `AUTH_NOT_SIGNED_IN`       | 401    | Call `sdk.signIn()`.                                                     |
| `AUTH_CHALLENGE_FAILED`    | 500    | Check publishable key and network. Retry.                                |
| `AUTH_VERIFICATION_FAILED` | 401    | User may have cancelled signing or wallet address mismatch.              |
| `AUTH_TOKEN_EXPIRED`       | 401    | JWT expired. The SDK auto-refreshes, but call `signIn()` if it persists. |
| `AUTH_INVALID_SESSION`     | 400    | Malformed data in `restoreSession()`. Clear and sign in again.           |

## Accounts

### Resolution

How Blend maps wallet addresses to accounts differs by SDK.

**Frontend (automatic):** The publishable key identifies your organization and account type. SIWE proves wallet ownership. After sign-in, the account is implicit.

**Server (manual):** The API key identifies your organization. The `accountTypeId` in the SDK config selects the product. You call `sdk.lookupAccount(address)` to resolve the account (creates it if new, but does not deploy a Safe). Then call `sdk.forAccount(accountId)` to scope operations, and `safe.request(chainId)` to deploy a Safe.

### Safe submodule

Check and trigger Safe deployments on specific chains.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Check if Safe is deployed on a chain
const result = await sdk.account.safe.resolve(8453);
```

`resolve` returns a discriminated union:

| Status           | Meaning                                                                                              |
| ---------------- | ---------------------------------------------------------------------------------------------------- |
| `"validated"`    | Safe exists on this chain. SDK 3.0.1 exposes `safeAddress` on this branch.                           |
| `"not-deployed"` | Safe hasn't been deployed on this chain yet.                                                         |
| `"invalid"`      | Safe ownership or configuration validation failed. Returns `error`.                                  |
| `"disconnected"` | Safe is deployed, but its role-modifier module is missing. Returns `safeAddress` and `moduleNeeded`. |

The REST response for `"validated"` also includes `accountId`, `userAddress`, and `chainId`. Those fields are not declared on the SDK 3.0.1 `SafeResolution` type. Use the authenticated account context when writing TypeScript against this release.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Request deployment on a new chain
await sdk.account.safe.request(8453);
```

`request` is fire-and-forget. Deployment happens in the background.

## Balance & positions

### balance

Current aggregate balance with per-chain breakdown.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const balance = await sdk.account.balance();
```

| Field         | Type                | Description                                |
| ------------- | ------------------- | ------------------------------------------ |
| `accountId`   | `string`            | Blend account ID                           |
| `safeAddress` | `string`            | On-chain Safe address                      |
| `perChain`    | `BalancePerChain[]` | Per-chain vault breakdown with fiat values |
| `total`       | `FiatAmount`        | Aggregate balance across all chains        |
| `heldAssets`  | `HeldAsset[]`       | Assets currently held in the Safe          |

Each `perChain` entry includes `totalUnderlying` in the token's smallest unit and `totalUnderlyingDecimals`. Use `getTotalUnderlyingBalance` to normalize those values and sum them without floating-point arithmetic:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { getTotalUnderlyingBalance } from "@blend-money/fe";

const balance = await sdk.account.balance();
const underlying = getTotalUnderlyingBalance(balance); // e.g. "501.5"
```

Server integrations import the same helper from `@blend-money/node`.

### balanceHistory

Time-series balance snapshots. Use `startDate` and `endDate` to filter.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const history = await sdk.account.balanceHistory({
  startDate: "2025-01-01",
  endDate: "2025-03-01",
});
```

### positions

All position events (deposits, withdrawals, rebalances) sorted newest first.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const positions = await sdk.account.positions();
```

`positions.events` is a discriminated union. Every event includes matching `kind` and `eventType` fields.

| Event       | Shared fields                                                                       | Event-specific fields                                                 |
| ----------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `DEPOSIT`   | `safeAddress`, `chainId`, `blockNumber`, `transactionHash`, `logIndex`, `blockTime` | `amount`, `price`, `tokenAddress`, `tokenSymbol`, `tokenDecimals`     |
| `WITHDRAW`  | Deposit fields                                                                      | `vaultAddress`, `intendedRecipient`, `assetAddress`, `fee`, `bridged` |
| `REBALANCE` | `safeAddress`, `chainId`, `blockNumber`, `transactionHash`, `logIndex`, `blockTime` | `vaultAddress`, `swaps`, `usdValueIn`, `usdValueOut`                  |

Narrow on either discriminator:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
for (const event of positions.events) {
  if (event.kind === "REBALANCE") {
    console.log(event.swaps, event.usdValueIn, event.usdValueOut);
  } else {
    console.log(event.amount, event.price, event.tokenSymbol);
  }
}
```

For deposit and withdrawal events, `price` is the fiat value of the full event, not a per-token unit price. Rebalance events use `usdValueIn` and `usdValueOut` instead of `price`.

### returns

Profit and loss metrics for the account.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const returns = await sdk.account.returns();
```

| Field            | Type         | Description                                                      |
| ---------------- | ------------ | ---------------------------------------------------------------- |
| `accountId`      | `string`     | Blend account UUID                                               |
| `current`        | `FiatAmount` | Current balance value                                            |
| `totalDeposited` | `FiatAmount` | Sum of all deposits                                              |
| `totalWithdrawn` | `FiatAmount` | Sum of all withdrawals                                           |
| `netDeposited`   | `FiatAmount` | `totalDeposited - totalWithdrawn`                                |
| `returns`        | `FiatAmount` | `current - netDeposited`                                         |
| `returnsPct`     | `number`     | `returns / netDeposited`, expressed as a ratio where `1` is 100% |

## Yield

Account-type-level yield data. Not per-account.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const yieldData = await sdk.discover.yield();
```

| Field            | Type                    | Description                                         |
| ---------------- | ----------------------- | --------------------------------------------------- |
| `accountTypeId`  | `string`                | The account type                                    |
| `yieldBreakdown` | `ChainYieldBreakdown[]` | Per-chain APY breakdown with base and boosted rates |

## Request cancellation

Every async SDK method that makes an HTTP request accepts `RequestOptions` as its final argument. Pass `signal` to cancel work when a user changes an input or a request is no longer needed.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const controller = new AbortController();

const quote = await sdk.quoteDeposit(
  {
    chainId: 8453,
    tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    amount: "100000000",
  },
  { signal: controller.signal },
);

controller.abort();
```

For server session methods, `RequestOptions` follows the intent parameters:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
await client.sessions.get(intentId, { signal: controller.signal });
```

## Error handling

### SdkError

All SDK methods throw `SdkError` on failure.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { SdkError } from "@blend-money/fe";

try {
  await sdk.quoteDeposit({ /* ... */ });
} catch (error) {
  if (error instanceof SdkError) {
    error.status;           // HTTP status code (0 for network)
    error.code;             // machine-readable code string
    error.message;          // human-readable description
    error.response;         // raw API response body
    error.isRetryable();    // true for 429, 5xx, network errors
    error.getUserMessage(); // user-friendly message
  }
}
```

Server integrations import `SdkError` from `@blend-money/node`. Both wrapper packages re-export the shared SDK types and utilities, so applications do not need to import `@blend-money/core` directly.

### Error codes

**Session errors:**

| Code                           | Status | Meaning                                                                    |
| ------------------------------ | ------ | -------------------------------------------------------------------------- |
| `FLOWPLAN_CONFLICT`            | 409    | Rebalance in progress. Retry after it settles.                             |
| `INTENT_NOT_FOUND`             | 404    | Session UUID not found.                                                    |
| `INTENT_EXPIRED`               | 410    | Quote has expired. Re-quote.                                               |
| `INTENT_WRONG_STATUS`          | 409    | Session cannot be modified in current state.                               |
| `INTENT_CONCURRENT_TRANSITION` | 409    | Session status changed concurrently. Retry.                                |
| `SESSION_NOT_QUOTED`           | 409    | Must quote before locking or executing.                                    |
| `SESSION_LOCKED_BY_OTHER`      | 409    | Locked by a different signer address.                                      |
| `SETTLEMENT_TIMEOUT`           | 408    | Polling a legacy `SUBMITTED` session exceeded the timeout (default 5 min). |

**Auth errors:**

| Code                       | Status | Meaning                                               |
| -------------------------- | ------ | ----------------------------------------------------- |
| `AUTH_NOT_SIGNED_IN`       | 401    | Not authenticated. Call `signIn()`.                   |
| `AUTH_CHALLENGE_FAILED`    | 500    | SIWE challenge request failed. Check publishable key. |
| `AUTH_VERIFICATION_FAILED` | 401    | Wallet signature verification failed.                 |
| `AUTH_TOKEN_EXPIRED`       | 401    | JWT expired. Auto-refresh will attempt re-auth.       |
| `AUTH_INVALID_SESSION`     | 400    | Malformed session data in `restoreSession()`.         |

**General errors:**

| Code                          | Status | Meaning                        |
| ----------------------------- | ------ | ------------------------------ |
| `NETWORK_ERROR`               | 0      | No response from server.       |
| `VALIDATION_ERROR`            | 400    | Client-side validation failed. |
| `RATE_LIMITED` / `RATE_LIMIT` | 429    | Too many requests.             |
| `NOT_FOUND`                   | 404    | Resource not found.            |
| `TIMEOUT`                     | 408    | Request timed out.             |
| `SERVER_ERROR`                | 500    | Internal server error.         |
| `NOT_IMPLEMENTED`             | 501    | Feature not available.         |

## Amount utilities

Convert between human-readable amounts and smallest-unit strings.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { parseAmount, formatAmount } from "@blend-money/fe";

parseAmount("100.50", 6);   // "100500000"  (USDC: 6 decimals)
parseAmount("1.5", 18);     // "1500000000000000000"  (18 decimals)

formatAmount("100500000", 6);           // "100.5"
formatAmount("1500000000000000000", 18); // "1.5"
```

`parseAmount` rejects fractional digits exceeding the token's decimal count.

Server integrations import these utilities from `@blend-money/node`.

## Key types

### SessionStatus

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type SessionStatus = "OPEN" | "LOCKED" | "SUBMITTED" | "SETTLED" | "FAILED" | "CANCELLED";
```

For new submissions, the persisted lifecycle is `OPEN` to `LOCKED` to `SETTLED`. The submit endpoint records accepted transaction hashes and settles the session directly. `SETTLED` does not mean Blend verified transaction receipts. Use balances and positions as the source of truth for indexed on-chain activity.

`SUBMITTED` and `FAILED` remain in the type for historical sessions and compatible resume behavior. An executor can poll a legacy `SUBMITTED` session until it reaches a terminal state. Terminal states are `SETTLED`, `FAILED`, and `CANCELLED`.

### DepositQuote

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type DepositQuote = {
  readonly type: "DEPOSIT";
  readonly intentId: string;
  readonly originChainId: number;
  readonly destinationChainId: number;
  readonly input: { readonly symbol: string; readonly amount: string; readonly amountUsd: string };
  readonly output: { readonly symbol: string; readonly amount: string; readonly amountUsd: string };
  readonly fees: { readonly totalUsd: string };
  readonly estimatedSeconds: number;
  readonly expiresAt: string;
};
```

### WithdrawQuote

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type WithdrawQuote = {
  readonly type: "WITHDRAW";
  readonly intentId: string;
  readonly destinationChainId: number;
  readonly totalAmount: string;
  readonly totalFeesUsd: string;
  readonly estimatedSeconds: number;
  readonly sourceChainCount: number;
  readonly sourceChainIds: readonly number[];
  readonly expiresAt: string;
};
```

### ExecuteResult

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type ExecuteResult = {
  status: "settled" | "failed" | "cancelled";
  txHashes: Array<{ hash: string; chainId: number }>;
  settledAt: string | null;
  error: string | null;
};
```

### ActionPlan

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type ActionPlan = {
  deployType: "direct" | "multisend";
  requiredApprovals: Txn[];
  requiredTxns: Txn[];
  chainId: number;
};
```

Deposits produce a single `ActionPlan` with `deployType: "direct"`. Withdrawals produce an `ActionPlan[]` with `deployType: "multisend"`.

### BalanceResponse

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
type BalanceResponse = {
  accountId: string;
  safeAddress: string;
  perChain: BalancePerChain[];
  total: FiatAmount;
  heldAssets: HeldAsset[];
};
```

<CardGroup cols={2}>
  <Card title="Frontend SDK" icon="browser" href="/build/sdk/frontend">
    Frontend configuration, auth, and execution.
  </Card>

  <Card title="Server SDK" icon="server" href="/build/sdk/server">
    Server configuration, account management, and sessions.
  </Card>
</CardGroup>
