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.
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
});
}
}
}
Sponsored Agent Execution
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