Skip to main content
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.
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.
Rotating your organization’s signing key invalidates all active SIWE sessions. Plan key rotation during low-traffic windows.

Session management

Deposits and withdrawals run through stateful sessions. One active session per account at a time. Re-quoting: Call quoteDeposit or quoteWithdraw again on the same OPEN session to update amounts and prices. Do not create a new session for re-quotes.
// 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 is for starting a completely new flow, not for updating a quote. Session expiration TTLs:
StateTTLOn expiry
OPEN~15 minutesAuto-cancelled
LOCKED~1 hourAuto-cancelled
SUBMITTED~1 hourMarked as failed
Build your UI to show quote expiration and allow re-quoting while the session is OPEN.

Transaction handling

Wait between action plans. The SDK calls waitForNextBlock() between sequential action plans. If you build custom execution logic, do the same. Submitting two plans in the same block can cause nonce collisions. Multi-chain withdrawal sequencing. Withdrawals can produce multiple action plans (one per source chain). Process them in sequence, not in parallel.
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 }];
  },
});
Paymaster sponsorship. Both SDKs require an ERC-4337 paymaster for gas-sponsored withdrawals. Configure paymasterUrl with a chain-aware resolver.
import { getPimlicoPaymasterUrl } from "@blend-money/core";

const paymasterUrl = getPimlicoPaymasterUrl("your-pimlico-api-key");
// Returns: (chainId) => `https://api.pimlico.io/v2/${chainId}/rpc?apikey=...`

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.
import { SdkError } from "@blend-money/core";

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;
}
Flow plan conflicts only affect withdrawals. Deposits are not blocked by active rebalances.

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:
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.
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:
StatusMeaning
"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"Could not reach the chain RPC. Retry.

Error recovery

The execute method in both SDKs is crash-safe. If your app crashes mid-execution, calling execute again with the same intent resumes from the current session state. It does not restart from scratch.
import { SdkError } from "@blend-money/core";

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

Frontend SDK

See the full frontend SDK reference.

Server SDK

See the full server SDK reference.
Last modified on May 28, 2026