Skip to main content
This guide walks through building a production-ready agent from scratch.

Architecture Overview

+------------------------------------------------------------------+
|                         YOUR APPLICATION                         |
+------------------------------------------------------------------+
| Frontend (User Grants Permission)                                |
| - sessionClient.grantPermissionTypedDataSign()                   |
+------------------------------------------------------------------+
| Backend (Agent Executes)                                         |
| - Store agent private key securely                               |
| - Store sessionDetails per user                                  |
| - agentClient.usePermission()                                    |
+------------------------------------------------------------------+
|                       BICONOMY MEE NETWORK                       |
| - Executes transactions, enforces policies onchain               |
+------------------------------------------------------------------+

Step 1: Setup

Install Dependencies

npm install @biconomy/abstractjs viem

Initialize SDK

import {
  createMeeClient,
  toMultichainNexusAccount,
  toSmartSessionsModule,
  meeSessionActions,
  getMEEVersion,
  MEEVersion
} from "@biconomy/abstractjs";
import { privateKeyToAccount } from "viem/accounts";
import { base, optimism, arbitrum } from "viem/chains";
import { http } from "viem";

Step 2: Generate Agent Key

Your agent needs its own keypair. Store this securely in your backend.
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";

// Generate once, store securely (e.g., AWS Secrets Manager, Vault)
const agentPrivateKey = generatePrivateKey();
const agentSigner = privateKeyToAccount(agentPrivateKey);

console.log("Agent address:", agentSigner.address);
// This address is what users will grant permissions to
Never expose the agent private key. Treat it like a production database password.

Step 3: User Onboarding (Frontend)

When a user wants to enable your agent, they grant permissions.

Create User’s Account & Session Client

// User's signer (from their wallet - Privy, MetaMask, etc.)
const userSigner = /* from wallet connection */;

// Create the user's multichain account
const userAccount = await toMultichainNexusAccount({
  signer: userSigner,
  chainConfigurations: [
    { chain: base, transport: http(), version: getMEEVersion(MEEVersion.V2_1_0) },
    { chain: optimism, transport: http(), version: getMEEVersion(MEEVersion.V2_1_0) },
    { chain: arbitrum, transport: http(), version: getMEEVersion(MEEVersion.V2_1_0) }
  ]
});

// Create MEE client with session actions
const meeClient = await createMeeClient({ account: userAccount });
const sessionClient = meeClient.extend(meeSessionActions);

Prepare Account (One-Time)

Install the Smart Sessions module on the user’s account:
const sessionsModule = toSmartSessionsModule({ signer: agentSigner });

const prepareResult = await sessionClient.prepareForPermissions({
  smartSessionsValidator: sessionsModule,
  feeToken: { address: USDC, chainId: base.id }
});

if (prepareResult) {
  await meeClient.waitForSupertransactionReceipt({ hash: prepareResult.hash });
}

Grant Permissions

import { getSudoPolicy, getUniversalActionPolicy } from "@biconomy/abstractjs";

// Define what your agent can do
const sessionDetails = await sessionClient.grantPermissionTypedDataSign({
  redeemer: agentSigner.address,  // Your agent's address
  feeToken: { address: USDC, chainId: base.id },
  
  // Time limits
  sessionValidAfter: Math.floor(Date.now() / 1000),
  sessionValidUntil: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,  // 30 days
  
  // Max gas spend
  maxPaymentAmount: parseUnits("20", 6),  // 20 USDC for gas
  
  // Allowed actions
  actions: [
    {
      chainId: base.id,
      actionTarget: MORPHO_VAULT,
      actionTargetSelector: toFunctionSelector("deposit(uint256,address)"),
      actionPolicies: [getSudoPolicy()]
    },
    {
      chainId: base.id,
      actionTarget: MORPHO_VAULT,
      actionTargetSelector: toFunctionSelector("withdraw(uint256,address,address)"),
      actionPolicies: [getSudoPolicy()]
    }
  ]
});

// IMPORTANT: Send sessionDetails to your backend
await sendToBackend(sessionDetails, userAccount.addressOn(base.id));

Step 4: Store Session Data (Backend)

Your backend needs to store session details for each user:
// Example database schema
interface UserSession {
  userId: string;
  accountAddress: string;  // User's smart account address
  sessionDetails: string;  // JSON stringified session details
  createdAt: Date;
  expiresAt: Date;
}

// Store session
async function storeSession(userId: string, accountAddress: string, sessionDetails: any) {
  await db.userSessions.insert({
    userId,
    accountAddress,
    sessionDetails: JSON.stringify(sessionDetails),
    createdAt: new Date(),
    expiresAt: new Date(sessionDetails.sessionValidUntil * 1000)
  });
}

Step 5: Agent Execution (Backend)

Your agent can now act on behalf of users:
async function executeAgentAction(userId: string, action: AgentAction) {
  // 1. Load user's session
  const session = await db.userSessions.findOne({ userId });
  const sessionDetails = JSON.parse(session.sessionDetails);
  
  // 2. Create agent's MEE client
  const agentAccount = await toMultichainNexusAccount({
    signer: agentSigner,
    chainConfigurations: [
      { chain: base, transport: http(), version: getMEEVersion(MEEVersion.V2_1_0) }
    ],
    accountAddress: session.accountAddress  // User's account address
  });
  
  const agentMeeClient = await createMeeClient({ account: agentAccount });
  const agentSessionClient = agentMeeClient.extend(meeSessionActions);
  
  // 3. Build the instruction
  const instruction = await agentAccount.buildComposable({
    type: "default",
    data: {
      chainId: base.id,
      to: MORPHO_VAULT,
      abi: MorphoAbi,
      functionName: "deposit",
      args: [action.amount, session.accountAddress]
    }
  });
  
  // 4. Execute with permission
  const result = await agentSessionClient.usePermission({
    sessionDetails,
    mode: "ENABLE_AND_USE",
    feeToken: { address: USDC, chainId: base.id },
    instructions: [instruction]
  });
  
  // 5. Wait for confirmation
  const receipt = await agentMeeClient.waitForSupertransactionReceipt({
    hash: result.hash
  });
  
  return receipt;
}

Step 6: Agent Logic

Implement your agent’s decision-making:

Yield Optimization Agent

async function runYieldOptimizer() {
  // Get all users with active sessions
  const users = await db.userSessions.find({
    expiresAt: { $gt: new Date() }
  });
  
  for (const user of users) {
    // Check current positions
    const positions = await getPositions(user.accountAddress);
    
    // Find best yield opportunity
    const bestOpportunity = await findBestYield();
    
    // Rebalance if significantly better
    if (bestOpportunity.apy > positions.currentApy * 1.1) {
      // Withdraw from current
      await executeAgentAction(user.userId, {
        type: "withdraw",
        protocol: positions.currentProtocol,
        amount: positions.amount
      });
      
      // Deposit to better opportunity
      await executeAgentAction(user.userId, {
        type: "deposit",
        protocol: bestOpportunity.protocol,
        amount: positions.amount
      });
    }
  }
}

// Run every hour
setInterval(runYieldOptimizer, 60 * 60 * 1000);

Trading Agent

async function runTradingAgent() {
  const users = await getActiveUsers();
  
  for (const user of users) {
    const signal = await analyzeMarket(user.preferences);
    
    if (signal.shouldTrade) {
      await executeAgentAction(user.userId, {
        type: "swap",
        tokenIn: signal.fromToken,
        tokenOut: signal.toToken,
        amount: signal.amount
      });
    }
  }
}
If you want to pay gas for your users:
const result = await agentSessionClient.usePermission({
  sessionDetails,
  mode: "ENABLE_AND_USE",
  sponsorship: true,  // You pay gas
  instructions: [instruction]
});
When using sponsorship: true, don’t set feeToken when granting permissions.

Error Handling

async function safeExecute(userId: string, action: AgentAction) {
  try {
    return await executeAgentAction(userId, action);
  } catch (error) {
    if (error.code === "PERMISSION_DENIED") {
      // Session expired or revoked
      await notifyUser(userId, "Please renew agent permissions");
      await db.userSessions.delete({ userId });
    } else if (error.code === "LIMIT_EXCEEDED") {
      // Spending or usage limit hit
      await notifyUser(userId, "Agent has reached its limits");
    } else if (error.code === "SIMULATION_FAILED") {
      // Transaction would revert
      console.error("Action would fail:", error.message);
    } else {
      throw error;
    }
  }
}

Monitoring & Observability

// Log all agent actions
async function executeWithLogging(userId: string, action: AgentAction) {
  const startTime = Date.now();
  
  try {
    const result = await executeAgentAction(userId, action);
    
    await logAgentAction({
      userId,
      action,
      status: "success",
      txHash: result.hash,
      duration: Date.now() - startTime
    });
    
    return result;
  } catch (error) {
    await logAgentAction({
      userId,
      action,
      status: "failed",
      error: error.message,
      duration: Date.now() - startTime
    });
    
    throw error;
  }
}

Security Checklist

Agent private key stored in secure secret management
Session details stored encrypted in database
Rate limiting on agent actions
Monitoring for unusual activity
Automatic session cleanup on expiry
User notification system for important events