Skip to main content
Use Notus smart wallets together with Deframe so users run yield strategy flows as ERC-4337 user operations: Deframe returns the exact calldata sequence for a strategy action; Notus wraps that sequence in a custom user operation, collects an EOA signature, and submits execution asynchronously. This guide follows the same Deframe patterns as the other external integrations (list strategies, request /bytecode), then adds the Notus-specific steps: resolve or register the smart wallet address, create a custom user operation from the bytecode list, sign userOperationHash, and call execute.

Resources

ResourceDescription
Deframe DocsStrategy listing and bytecode generation
Register Smart WalletNotus POST /api/v1/wallets/register
Get Smart WalletNotus GET /api/v1/wallets/address
Create Custom User OperationNotus POST /api/v1/crypto/custom-user-operation
Execute User OperationNotus POST /api/v1/crypto/execute-user-op

Prerequisites

Configure your backend or secure client with:
  • Deframe: base URL and x-api-key (same as other integrations).
  • Notus: X-Api-Key header on every Notus request, plus the factory address and salt your project uses so the counterfactual smart wallet is deterministic for each EOA.
Notus uses https://api.notus.team/api/v1 as the API prefix (combine with the paths below).
Never expose the Notus API key or Deframe API key in public client bundles unless you accept the risk of key abuse. Prefer a backend that proxies these calls.

Reference client

The snippets below use notusPost, getSmartWalletFromNotus, createSmartWalletFromNotus, and signRawHash as shorthand. They are not part of Deframe — implement them in your app (for example with axios and viem WalletClient), following the same HTTP paths and headers as a typical Notus integration. The structure below mirrors a small OOP-style client adapted from common Notus + viem setups (request builder, X-Api-Key, factory / salt, raw-hash signing for userOperationHash):
// Pseudocode — illustrative only; tighten types, validation, and errors for production.
import type { WalletClient } from "viem";

type NotusClientConfig = {
  /** e.g. https://api.notus.team/api/v1 — include `/api/v1` if your paths omit it */
  baseUrl: string;
  apiKey: string;
  factory: string;
  salt: string;
};

type NotusWalletDto = {
  wallet?: {
    accountAbstraction?: string;
    registeredAt?: string | null;
  };
};

export class NotusIntegrationClient {
  #cfg: NotusClientConfig;

  constructor(cfg: NotusClientConfig) {
    this.#cfg = cfg;
  }

  /** POST helper used by this guide for `/crypto/*` and `/wallets/register`. */
  async notusPost<T>(path: string, body: object): Promise<T> {
    const url = `${this.#cfg.baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
    const res = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Api-Key": this.#cfg.apiKey,
      },
      body: JSON.stringify(body),
    });
    if (!res.ok) throw new Error(await res.text());
    return res.json() as Promise<T>;
  }

  /** GET `/wallets/address` — returns smart wallet address only when already registered. */
  async getSmartWalletFromNotus({
    address,
  }: {
    address: string;
  }): Promise<{ smartWallet: string } | null> {
    const q = new URLSearchParams({
      externallyOwnedAccount: address,
      factory: this.#cfg.factory,
      salt: String(this.#cfg.salt),
    });
    const url = `${this.#cfg.baseUrl.replace(/\/$/, "")}/wallets/address?${q}`;
    const res = await fetch(url, {
      headers: { "X-Api-Key": this.#cfg.apiKey },
    });
    if (!res.ok) return null;
    const data = (await res.json()) as NotusWalletDto;
    const w = data.wallet;
    if (w?.accountAbstraction && w?.registeredAt)
      return { smartWallet: w.accountAbstraction };
    return null;
  }

  /** POST `/wallets/register` — links factory + salt wallet to your Notus project. */
  async createSmartWalletFromNotus({
    address,
  }: {
    address: string;
  }): Promise<{ smartWallet: string } | null> {
    const data = await this.notusPost<NotusWalletDto>("/wallets/register", {
      externallyOwnedAccount: address,
      factory: this.#cfg.factory,
      salt: this.#cfg.salt,
    });
    const w = data.wallet;
    if (w?.accountAbstraction && w?.registeredAt)
      return { smartWallet: w.accountAbstraction };
    return null;
  }

  /**
   * Sign `userOperationHash` as raw bytes (per Notus execute flow).
   * Same idea as viem: signMessage({ message: { raw: hash } }).
   */
  async signRawHash(
    walletClient: WalletClient,
    userOperationHash: `0x${string}`,
  ): Promise<`0x${string}`> {
    return walletClient.signMessage({
      account: walletClient.account!,
      message: { raw: userOperationHash },
    });
  }
}
You can extend this class with buildCustomUserOperation / sendUserOperation methods that call notusPost("/crypto/custom-user-operation", …) and notusPost("/crypto/execute-user-op", …) when you want a single place for all Notus traffic.

Integration pattern

1. Resolve the smart wallet (get or register)

The EOA is the address that will sign user operations. The smart wallet (account abstraction address) is derived from the EOA, factory, and salt. Recommended flow:
  1. GET /wallets/address?externallyOwnedAccount=...&factory=...&salt=...Get Smart Wallet.
  2. If the wallet is not registered yet (registeredAt missing or your product rules say so), POST /wallets/register with the same externallyOwnedAccount, factory, and optional saltRegister Smart Wallet.
From the JSON response, use wallet.accountAbstraction (or wallet.walletAddress when they match) as the walletAddress for Deframe bytecode requests and for Notus user operations. Conceptually:
const smartWallet =
  (await getSmartWalletFromNotus({ address: EOA }))?.smartWallet ??
  (await createSmartWalletFromNotus({ address: EOA }))?.smartWallet;
Only treat the address as usable for your app when Notus indicates registration succeeded (per your checks on registeredAt and error payloads such as FACTORY_NOT_SUPPORTED).

2. Request strategy bytecodes from Deframe

ParameterValue
MethodGET
Path/strategies/:strategy-id/bytecode
Queryaction (required), wallet (required — use the Notus smart wallet address), amount (required, integer string in token decimals)
Headersx-api-key: <Deframe API key>
The response includes bytecode[] with to, data, value, and chainId per step. All steps in one Notus custom user operation must run in order on a single chainId; use the first item’s chainId when building the operation (if Deframe returns multiple chains, split into separate Notus flows).
type DeframeBytecodeItem = {
  to: string;
  data: string;
  value: string;
  chainId: string;
};

3. Create a custom user operation on Notus

POST /crypto/custom-user-operationCreate Custom User Operation. Map each Deframe bytecode to a Notus operation:
DeframeNotus body field
tooperations[].address
dataoperations[].data
valueoperations[].value (hex string; use 0x0 when empty)
Also send:
  • chainId — number (e.g. 137 for Polygon; see Notus docs for supported networks).
  • walletAddress — the smart wallet address from step 1.
  • payGasFeeToken — ERC-20 contract address used to pay gas (required in the current API; pick the token Notus should debit for fees on that chain).
For a lend action where the bytecode spends the entire balance of a token, you usually cannot use that same token for payGasFeeToken: Notus would try to pull fees from a balance already consumed by the lend flow, and the user operation can revert. Prefer paying gas with the strategy asset (or another token that still has leftover balance), not the underlyingAsset when that is what you are fully deploying. For withdraw, paying gas with the underlyingAsset is typically fine because the flow increases that balance rather than draining it for the core action.
Example mapping:
const chainId = Number(bytecodes[0].chainId);
const operations = bytecodes.map(({ to, data, value }) => ({
  address: to,
  data,
  value: value ?? "0x0",
}));

const { userOperation } = await notusPost("/crypto/custom-user-operation", {
  chainId,
  operations,
  walletAddress: smartWallet,
  payGasFeeToken: gasTokenAddress,
});
The response includes userOperation.userOperationHash (and related fields). If Notus returns a revert reason preview, fix balances, allowances, or ordering before executing.

4. Sign and execute the user operation

  1. Sign the userOperationHash with the EOA that owns the smart wallet (raw message signature). With viem, sign the hash as raw bytes (not a human-readable EIP-191 message unless Notus specifies otherwise). Notus documents that authorization may be required on the first transaction for an EIP-7702 wallet: follow their note on viem signAuthorization() vs serializing the signature for the execute endpoint — Execute User Operation.
  2. POST /crypto/execute-user-op with:
    • userOperationHash
    • signature
    • authorization — only when required for that wallet / first operation
Execution is asynchronous (HTTP 202). Use Notus history or webhooks to track inclusion, as described in their API reference.
const signature = await signRawHash(
  walletClient,
  userOperation.userOperationHash,
);
await notusPost("/crypto/execute-user-op", {
  userOperationHash: userOperation.userOperationHash,
  signature,
  // authorization: '0x...' // when required
});

Operational notes

  • Single chain per custom user operation: Notus runs operations in order on one chainId. Multi-chain Deframe flows (metadata.isCrossChain) need a different orchestration than a single custom user operation.
  • Paymaster / gas token: Ensure payGasFeeToken and balances match Notus expectations for your project tier (errors such as UNAVAILABLE_COMPUTE_UNITS are documented on their side).
  • Smart wallet deployment: If the account is not yet deployed on the target chain, resolution or execution may fail until deployment rules for your factory are satisfied (see Notus factory compatibility and your dashboard settings).

Next steps

Privy Integration

Bytecode flow with embedded smart wallets

Dynamic Integration

Wallet auth and Deframe API usage

Deposit to Yield

End-user deposit guide

Fireblocks Integration

Custody integration