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

Architecture Overview

+------------------------------------------------------------------+
|                         YOUR APPLICATION                         |
+------------------------------------------------------------------+
| Frontend (User Prepares And Enable Permission)                   |             
| - SCA deployment if not already                                  |
| - Smart session validator installation if not already            |
| - Funds the SCA if required                                      |
| - Enable session permissions                                     |
+------------------------------------------------------------------+
| Backend (Agent Executes)                                         |
| - Store agent private key securely                               |
| - Store sessionDetails per user                                  |
| - Executes on behalf of user with enabled permissions            |
+------------------------------------------------------------------+
| BICONOMY MEE NETWORK                                             |
| - Executes transactions, enforces policies onchain               |
+------------------------------------------------------------------+

Step 1: Setup

Install Dependencies

npm install @biconomy/abstractjs viem

Initialize SDK

import { 
  createMeeClient, 
  getMEEVersion, 
  MEEVersion, 
  SessionDetail, 
  toMultichainNexusAccount 
} from "@biconomy/abstractjs";
import { Abi, Address, http, parseUnits, toFunctionSelector, encodeFunctionData } from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { arbitrum, base, optimism } from "viem/chains";

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 enable 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 activate your agent, they enable permissions.

Create User’s Account & Mee Client

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

console.log("User address:", userSigner.address);

// 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 });

Prepare And Enable Permissions

While user wants to enable permissions, they do the following operations
  • Deploying the Smart Contract Account (SCA) if it doesn’t exist yet.
  • Installing the Smart Session Validator module if not already present.
  • Funding the SCA if additional funds are required.
  • Granting (enabling) session permissions for the agent to act on the user’s behalf.
All in one supertransaction.
const MORPHO_VAULT: Address = "0x...";
const USDC: Address = "0x...";

// Define what your agent can do
const [depositAction] = userAccount.buildSessionAction({
    type: "custom",
    data: {
        chainIds: [base.id],
        contractAddress: MORPHO_VAULT,
        functionSignature: toFunctionSelector("deposit(uint256,address)"),
        // By default sudo policy will be applied
    }
});

const [withdrawAction] = userAccount.buildSessionAction({
    type: "custom",
    data: {
        chainIds: [base.id],
        contractAddress: MORPHO_VAULT,
        functionSignature: toFunctionSelector("withdraw(uint256,address,address)"),
        // By default sudo policy will be applied
    }
});

const [transferAction] = userAccount.buildSessionAction({
    type: "transfer",
    data: {
        chainIds: [base.id],
        contractAddress: USDC,
        recipientAddress: "0x...", 
        amountLimitPerAction: parseUnits("5", 6),
        maxAmountLimit: parseUnits("100", 6),
        usageLimit: 100n,
        validAfter: Math.floor(Date.now() / 1000),
        validUntil: Math.floor(Date.now() / 1000) + 60 * 60 * 24
    }
});

const prepareAndEnableSessionQuote = await meeClient.getSessionQuote({
    mode: "PREPARE",
    enableSession: {
        redeemer: agentSigner.address,
        // Allowed actions
        actions: [depositAction, withdrawAction, transferAction],
        // Max gas spend
        maxPaymentAmount: parseUnits("20", 6),  // 20 USDC for gas
    },
    simulation: { simulate: true },
    feeToken: { address: USDC, chainId: base.id },
    trigger: {
        tokenAddress: USDC,
        chainId: base.id,
        amount: parseUnits("10", 6), // $10 USDC funding
    },
});

// Store the sessions details for later use.
let sessionDetails: SessionDetail[] = [];

if (prepareAndEnableSessionQuote) {
    const { hash } = await meeClient.executeSessionQuote(prepareAndEnableSessionQuote);
    await meeClient.waitForSupertransactionReceipt({ hash });

    if (prepareAndEnableSessionQuote.sessionDetails) sessionDetails = prepareAndEnableSessionQuote.sessionDetails;
}

// 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: SessionDetail[];  // Can be JSON stringified while storing
  createdAt: Date;
  expiresAt: Date;
}

// Store session
async function storeSession(userId: string, accountAddress: string, sessionDetails: SessionDetail[]) {
  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: SessionDetail[] = 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 });

  // 3. Build the instruction
  const depositData = encodeFunctionData({
      abi: MORPHO_ABI,
      functionName: "deposit",
      args: [parseUnits("5", 6), session.accountAddress],
  });
  
  const instructions = await agentAccount.build({
      type: "default",
      data: [{
          calls: [{
              to: MORPHO_VAULT,
              data: depositData
          }],
          chainId: base.id,
      }]
  });
  
  // 4. Get session quote
  const quote = await agentMeeClient.getSessionQuote({
      mode: "USE",
      sessionDetails,
      simulation: { simulate: true },
      feeToken: { address: USDC, chainId: base.id },
      instructions
  });

  // 5. Execute with permission
  const { hash } = await agentMeeClient.executeSessionQuote(quote);
  
  // 6. Wait for confirmation
  const { receipts } = await agentMeeClient.waitForSupertransactionReceipt({ hash });

  
  return receipts;
}

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:
await agentMeeClient.getSessionQuote({
  mode: "USE",
  sessionDetails,
  simulation: { simulate: true },
  sponsorship: true,  // You pay gas
  instructions
});
When using sponsorship: true, don’t set feeToken when enabling permissions.

Monitoring & Observability

// Log all agent actions
async function executeWithLogging(userId: string, action: AgentAction) {
  const startTime = Date.now();
  
  try {
    const receipts = await executeAgentAction(userId, action);
    
    await logAgentAction({
      userId,
      action,
      status: "success",
      transactionHashes: receipts.map((receipt) => receipt.transactionHash),
      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