> ## 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.

# Best Practices

> Production patterns for credential security, session management, transaction handling, conflict resolution, Safe deployment, and error recovery.

A working SDK integration is step one. A production-quality integration handles edge cases, secures credentials, and recovers from failures.

## Credential security

Your `sk_live_` API key grants full access to your organization's accounts. Treat it like a database password.

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

// Load from environment, never hardcode
const sdk = new BlendServerSdk({
  apiKey: process.env.BLEND_API_KEY!,
  accountTypeId: "your-account-type-id",
});
```

* Store API keys in environment variables or a secrets manager
* Never log the full key value
* Create separate keys per environment (dev, staging, production)
* Rotate keys on a regular schedule, not only after incidents

Your publishable key (`pk_live_`) is safe for client-side code. It identifies your account type but cannot read or modify account data on its own. The SIWE session provides the authorization layer.

<Warning>Rotating your organization's signing key invalidates all active SIWE sessions. Plan key rotation during low-traffic windows.</Warning>

## Session management

Deposits and withdrawals run through stateful sessions. One active session per account at a time.

**Re-quoting:** Call the same quote method again on the current OPEN session to change the amount, token, chain, or current price. Do not create a new session for re-quotes.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Re-quote on the same session (correct)
const quote1 = await sdk.quoteDeposit({
  chainId: 8453,
  tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  amount: "1000000",
});

// User changes amount, re-quote same session
const quote2 = await sdk.quoteDeposit({
  chainId: 8453,
  tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  amount: "5000000",
});
```

**Force reset:** Use `forceReset: true` only when you need to cancel the existing session entirely. This includes switching between deposit and withdrawal in the high-level frontend SDK. Do not use it to update a same-type quote.

**Session expiration TTLs:**

| State      | TTL          | On expiry      |
| ---------- | ------------ | -------------- |
| **OPEN**   | \~15 minutes | Auto-cancelled |
| **LOCKED** | \~1 hour     | Auto-cancelled |

Build your UI to show quote expiration and allow re-quoting while the session is OPEN.

New submissions move directly from `LOCKED` to `SETTLED` when the API accepts the transaction hashes. `SETTLED` confirms hash ingestion, not receipt verification. Use balances and positions to confirm indexed on-chain activity.

The SDK retains `SUBMITTED` and `FAILED` for historical sessions. When resuming a legacy `SUBMITTED` session, an executor polls until it reaches a terminal state. Do not design new flows around entering `SUBMITTED`.

## Transaction handling

The frontend and server execution helpers have different responsibilities:

* `sdk.execute()` in `@blend-money/fe` derives wallet clients, submits Safe transactions using the configured paymaster registry, and records their hashes.
* `client.sessions.execute()` in `@blend-money/node` orchestrates the session lifecycle, but your `submitActionPlan` callback owns signing and on-chain submission.

Withdrawals can produce multiple action plans, one per source chain. The server executor invokes `submitActionPlan` sequentially for each plan. Keep your callback idempotent and return every submitted hash with its `chainId`.

```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: "your-account-type-id",
});

const client = sdk.forAccount(accountId);

// execute() handles sequencing automatically
const session = await client.sessions.execute(intentId, {
  signerAddress: eoa,
  submitActionPlan: async (plan) => {
    // Your on-chain execution logic here
    return [{ hash: txHash, chainId: plan.chainId }];
  },
});
```

The server response normally returns `SETTLED` immediately after hash submission. SDK 3.0.1 can still surface a `SUBMITTED` status callback for compatibility, but new server sessions do not persist that intermediate state.

**Paymaster sponsorship.** Only the frontend SDK accepts paymaster configuration. Configure its required per-chain registry with the current Pimlico and Alchemy helpers.

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

const sdk = new BlendSdk({
  publishableKey: "pk_live_abc123...",
  signMessage: (message) => walletClient.signMessage({ message }),
  paymaster: {
    ...getPimlicoPaymasterEndpoints("pimlico-key", [8453]),
    ...getAlchemyPaymasterEndpoints(
      "alchemy-key",
      { policyId: "gas-manager-policy-id" },
      [137],
    ),
  },
});
```

Later registry entries replace earlier entries for the same chain. `BlendServerSdk` has no paymaster option. Its `submitActionPlan` callback is responsible for signing, gas, and transaction submission.

## Conflict handling

A `409` with `FLOWPLAN_CONFLICT` means a rebalance is in progress on the account. The system is moving funds between vaults.

Do not retry blindly. Surface this as a product-level message and let the user retry after the account settles.

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

try {
  const quote = await sdk.quoteWithdraw({
    destinationChainId: 8453,
    amount: "1000000",
  });
} catch (error) {
  if (error instanceof SdkError && error.code === "FLOWPLAN_CONFLICT") {
    // Show: "Your account is being rebalanced. Try again in a few minutes."
    return;
  }
  throw error;
}
```

<Tip>Flow plan conflicts only affect withdrawals. Deposits are not blocked by active rebalances.</Tip>

## Safe deployment

Safes are deployed lazily. Creating an account does not deploy the Safe on every chain. The `chainsDeployed` array in the account response tells you which chains are live right now.

This catches most people off guard: signing in or looking up an account does not deploy a Safe. The `chainId` in `signIn()` is only for the SIWE challenge message. Deploying a Safe is always a separate `safe.request()` call, and it happens asynchronously.

Before your first transaction on any chain, check deployment and request it if needed:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const client = sdk.forAccount(accountId);
const resolved = await client.account.safe.resolve(137); // Polygon

if (resolved.status === "not-deployed") {
  await client.account.safe.request(137);
}
```

`safe.request()` is fire-and-forget. Deployment happens in the background. Poll `safe.resolve()` until the status changes to `"validated"` before executing transactions.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
async function ensureSafe(client, chainId) {
  let resolved = await client.account.safe.resolve(chainId);

  if (resolved.status === "not-deployed") {
    await client.account.safe.request(chainId);

    while (resolved.status === "not-deployed") {
      await new Promise((r) => setTimeout(r, 2000));
      resolved = await client.account.safe.resolve(chainId);
    }
  }

  return resolved;
}
```

Your Safe has the same address on every chain. Once deployed on one chain, you can deploy on any other chain and get the same address. The CREATE2 determinism makes cross-chain reuse automatic.

`safe.resolve()` returns one of four statuses:

| Status           | Meaning                                                                                              |
| ---------------- | ---------------------------------------------------------------------------------------------------- |
| `"validated"`    | Safe exists on this chain. Ready for transactions.                                                   |
| `"not-deployed"` | Safe hasn't been deployed on this chain yet. Call `safe.request()`.                                  |
| `"invalid"`      | Contract at the address is not a valid Safe.                                                         |
| `"disconnected"` | Safe is deployed, but its role-modifier module is missing. Inspect `safeAddress` and `moduleNeeded`. |

## Error recovery

Both execution helpers read the current server session before deciding what to do next. Reusing the same intent resumes from the state the API recorded. Make on-chain submission idempotent because a process can stop after broadcasting a transaction but before recording its hash with Blend.

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

try {
  await sdk.execute(quote, {
    signerAddress: eoa,
    deriveSigner: async (chainId) => ({
      signer: getWalletClient({ chainId, account: eoa }),
      publicClient: getPublicClient({ chainId }),
    }),
  });
} catch (error) {
  if (error instanceof SdkError && error.isRetryable()) {
    // Safe to retry: 429, 5xx, network errors
    // Exponential backoff built into HTTP client
  }
}
```

The derived wallet client must be configured for `chainId` with an attached account whose address equals `signerAddress`.

For server integrations, import `SdkError` from `@blend-money/node` and retry `client.sessions.execute()` with the same intent ID. A retry must not broadcast a duplicate transaction when your own records already contain its hash.

`isRetryable()` returns true for HTTP 429, 5xx status codes, and network errors (status 0). The built-in HTTP client already retries with exponential backoff and jitter, so you only need manual retry logic for application-level recovery.

<CardGroup cols={2}>
  <Card title="Frontend SDK" icon="browser" href="/build/sdk/frontend">
    See the full frontend SDK reference.
  </Card>

  <Card title="Server SDK" icon="server" href="/build/sdk/server">
    See the full server SDK reference.
  </Card>
</CardGroup>
