Skip to main content
Attach runtime conditions to any composable transaction. Transactions wait until conditions are met before execution - enabling limit orders, price-based triggers, and safe conditional operations.

Why Use Conditional Execution?

Traditional approach: Execute immediately and hope conditions are met, or build complex smart contracts
With conditions: Transactions wait intelligently until requirements are satisfied, then execute automatically

Key Benefits

  • Smart waiting - Transactions stay PENDING until conditions are met (limit orders, price triggers, etc.)
  • User protection - Only execute when safe (sufficient balance, healthy positions, etc.)
  • MEV protection - Check price slippage before swaps execute
  • Gas efficient - Avoid failed transactions by verifying conditions first
  • Works everywhere - Compatible with ALL instruction types (transfer, approve, default, etc.)
  • Type-safe - Full TypeScript support with ABI inference

Common Use Cases

  • Limit orders - Wait for price to reach target level before executing
  • Balance-triggered actions - Execute when funds arrive from bridges or swaps
  • Safety checks - Verify contract state (not paused) before execution
  • DeFi safeguards - Ensure healthy positions before borrowing or withdrawing
  • Slippage protection - Only execute swaps at acceptable prices

Quick Start

1

Install the SDK

npm install @biconomy/abstractjs@latest viem
2

Create a Condition

import { createCondition, ConditionType } from '@biconomy/abstractjs'
import { erc20Abi, parseUnits } from 'viem'

// Only execute if balance >= 100 USDC
const minBalanceCondition = createCondition({
  targetContract: usdcAddress,
  functionAbi: erc20Abi,
  functionName: 'balanceOf',
  args: [userAddress],
  value: parseUnits('100', 6), // 100 USDC (6 decimals)
  type: ConditionType.GTE,
  description: 'Minimum balance: 100 USDC'
})
3

Add to Any Instruction

const instructions = await mcAccount.buildComposable({
  type: 'transfer',
  data: {
    tokenAddress: usdcAddress,
    recipient: recipientAddress,
    amount: transferAmount,
    chainId: base.id,
    conditions: [minBalanceCondition] // ✅ Add here
  }
})
4

Execute

const quote = await meeClient.getQuote({
  instructions,
  feeToken: { chainId: base.id, address: usdcAddress }
})

const { hash } = await meeClient.executeQuote({ quote })
// Transaction waits (PENDING) until condition is met
// Then executes automatically when balance >= 100 USDC

How It Works

Conditions leverage the composability system’s STATIC_CALL mechanism with intelligent waiting:
User Submits Transaction with Conditions

MEE checks: Condition 1 (STATIC_CALL) → Check constraint

   Pass? → Continue
   Fail? → Transaction enters PENDING state

MEE periodically rechecks conditions

All conditions pass? → Execute main function

Success!
Two execution paths:
  1. Immediate execution - All conditions pass → Transaction executes immediately
  2. Waiting mode - Conditions not met → Transaction stays PENDING, MEE periodically rechecks until conditions pass or timeout expires
Under the hood: Each condition becomes an InputParam that calls a view/pure function and validates the result against your constraint (GTE, LTE, or EQ). MEE simulates execution periodically, and once simulation succeeds (all conditions pass), the transaction executes onchain.

API Reference

createCondition

Create a condition that must be satisfied for execution.
createCondition({
  targetContract: Address        // Contract to call
  functionAbi: Abi              // ABI containing the function
  functionName: string          // View/pure function name
  args: Array<any>              // Function arguments (type-safe)
  value: bigint                 // Threshold or expected value
  type: ConditionType           // GTE, LTE, or EQ
  description?: string          // Optional description
})

ConditionType

enum ConditionType {
  GTE = "gte",  // Greater than or equal to (≥)
  LTE = "lte",  // Less than or equal to (≤)
  EQ = "eq"     // Equal to (=)
}
Examples:
// Balance must be at least 1000 tokens
type: ConditionType.GTE,
value: parseUnits('1000', 18)

// Health factor must not exceed 2.0
type: ConditionType.LTE,
value: parseUnits('2.0', 18)

// Contract must be in ACTIVE state (enum value 2)
type: ConditionType.EQ,
value: 2n

Common Patterns

Minimum Balance Check

const minBalance = parseUnits('50', 6) // 50 USDC

const instructions = await mcAccount.buildComposable({
  type: 'transfer',
  data: {
    tokenAddress: usdcAddress,
    recipient: recipientAddress,
    amount: runtimeERC20BalanceOf({
      targetAddress: senderAddress,
      tokenAddress: usdcAddress
    }),
    chainId: base.id,
    conditions: [
      createCondition({
        targetContract: usdcAddress,
        functionAbi: erc20Abi,
        functionName: 'balanceOf',
        args: [senderAddress],
        value: minBalance,
        type: ConditionType.GTE,
        description: 'Sender must have at least 50 USDC'
      })
    ]
  }
})

Contract Not Paused

const pausableAbi = [
  {
    inputs: [],
    name: 'paused',
    outputs: [{ name: '', type: 'bool' }],
    stateMutability: 'view',
    type: 'function'
  }
] as const

const instructions = await mcAccount.buildComposable({
  type: 'default',
  data: {
    to: protocolAddress,
    abi: protocolAbi,
    functionName: 'stake',
    args: [stakeAmount],
    chainId: base.id,
    conditions: [
      createCondition({
        targetContract: protocolAddress,
        functionAbi: pausableAbi,
        functionName: 'paused',
        args: [],
        value: 0n, // False = 0
        type: ConditionType.EQ,
        description: 'Protocol must not be paused'
      })
    ]
  }
})

Multiple Conditions (AND Logic)

All conditions must pass for execution.
const instructions = await mcAccount.buildComposable({
  type: 'default',
  data: {
    to: lendingProtocolAddress,
    abi: lendingProtocolAbi,
    functionName: 'borrow',
    args: [borrowParams],
    chainId: base.id,
    conditions: [
      // Condition 1: Sufficient collateral
      createCondition({
        targetContract: collateralTokenAddress,
        functionAbi: erc20Abi,
        functionName: 'balanceOf',
        args: [userAddress],
        value: minCollateralAmount,
        type: ConditionType.GTE
      }),

      // Condition 2: Healthy position
      createCondition({
        targetContract: lendingProtocolAddress,
        functionAbi: lendingProtocolAbi,
        functionName: 'getHealthFactor',
        args: [userAddress],
        value: parseUnits('1.5', 18),
        type: ConditionType.GTE
      }),

      // Condition 3: Protocol not paused
      createCondition({
        targetContract: lendingProtocolAddress,
        functionAbi: lendingProtocolAbi,
        functionName: 'isPaused',
        args: [],
        value: 0n,
        type: ConditionType.EQ
      })
    ]
  }
})
All conditions must pass - The transaction waits (PENDING) until all conditions are satisfied, or fails if timeout expires.
Create a limit order that waits for a price target.
const uniswapPoolAbi = [
  {
    inputs: [],
    name: 'slot0',
    outputs: [
      { name: 'sqrtPriceX96', type: 'uint160' },
      { name: 'tick', type: 'int24' },
      // ... other fields
    ],
    stateMutability: 'view',
    type: 'function'
  }
] as const

// Target price: swap only when price >= targetSqrtPriceX96
const targetPrice = calculateSqrtPriceX96(targetPriceRatio)

const instructions = await mcAccount.buildComposable({
  type: 'default',
  data: {
    to: uniswapRouterAddress,
    abi: uniswapRouterAbi,
    functionName: 'exactInputSingle',
    args: [swapParams],
    chainId: base.id,
    conditions: [
      createCondition({
        targetContract: poolAddress,
        functionAbi: uniswapPoolAbi,
        functionName: 'slot0',
        args: [],
        value: targetPrice,
        type: ConditionType.GTE, // Wait until price >= target
        description: 'Limit order: execute when price reaches target'
      })
    ]
  }
})

// Use getFusionQuote with upperBoundTimestamp to enable waiting
const fusionQuote = await meeClient.getFusionQuote({
  trigger: { chainId: base.id, tokenAddress: usdcAddress, amount: inputAmount },
  instructions,
  feeToken: { chainId: base.id, address: usdcAddress },
  upperBoundTimestamp: Math.floor(Date.now() / 1000) + 86400 // Wait up to 24 hours
})

const { hash } = await meeClient.executeFusionQuote({ fusionQuote })
// Transaction waits until price reaches target, then executes automatically
Check current price before executing swaps.
const uniswapPoolAbi = [
  {
    inputs: [],
    name: 'slot0',
    outputs: [
      { name: 'sqrtPriceX96', type: 'uint160' },
      { name: 'tick', type: 'int24' },
      // ... other fields
    ],
    stateMutability: 'view',
    type: 'function'
  }
] as const

// Calculate acceptable price range
const currentPrice = await getSpotPrice(poolAddress)
const maxAcceptablePrice = currentPrice * 101n / 100n // 1% slippage

const instructions = await mcAccount.buildComposable({
  type: 'default',
  data: {
    to: uniswapRouterAddress,
    abi: uniswapRouterAbi,
    functionName: 'exactInputSingle',
    args: [swapParams],
    chainId: base.id,
    conditions: [
      createCondition({
        targetContract: poolAddress,
        functionAbi: uniswapPoolAbi,
        functionName: 'slot0',
        args: [],
        value: maxAcceptablePrice,
        type: ConditionType.LTE,
        description: 'Price slippage protection'
      })
    ]
  }
})

Waiting for Conditions

When conditions aren’t immediately met, MEE keeps the transaction in PENDING state and periodically checks until conditions pass.
const minBalanceRequired = parseUnits('100', 6)

const instructions = await mcAccount.buildComposable({
  type: 'transfer',
  data: {
    tokenAddress: usdcAddress,
    recipient: recipientAddress,
    amount: transferAmount,
    chainId: base.id,
    conditions: [
      createCondition({
        targetContract: usdcAddress,
        functionAbi: erc20Abi,
        functionName: 'balanceOf',
        args: [senderAddress],
        value: minBalanceRequired,
        type: ConditionType.GTE
      })
    ]
  }
})

// Execute with timeout
const fusionQuote = await meeClient.getFusionQuote({
  trigger: { chainId: base.id, tokenAddress: usdcAddress, amount: triggerAmount },
  instructions,
  feeToken: { chainId: base.id, address: usdcAddress },
  upperBoundTimestamp: Math.floor(Date.now() / 1000) + 300 // Wait up to 5 min
})

const { hash } = await meeClient.executeFusionQuote({ fusionQuote })

// Transaction stays PENDING until condition is met
const { transactionStatus } = await meeClient.waitForSupertransactionReceipt({ hash })
Lifecycle:
  1. Submit transaction with conditions
  2. PENDING - MEE periodically checks conditions
  3. Condition satisfied - Transaction executes automatically
  4. MINED_SUCCESS - Complete
If conditions aren’t met within the timeout period, the transaction will eventually FAIL.

Best Practices

1. Always Add Descriptions

// ✅ Good
createCondition({
  targetContract: usdcAddress,
  functionAbi: erc20Abi,
  functionName: 'balanceOf',
  args: [userAddress],
  value: parseUnits('100', 6),
  type: ConditionType.GTE,
  description: 'User must have at least 100 USDC' // ✅ Clear
})

2. Use Type-Safe ABIs

// ✅ Good - Type-safe
const erc20Abi = [
  {
    inputs: [{ name: 'account', type: 'address' }],
    name: 'balanceOf',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function'
  }
] as const // ✅ 'as const' for type inference

3. Limit Number of Conditions

Each condition adds ~5,000-10,000 gas:
  • 1-3 conditions - Optimal
  • ⚠️ 4-5 conditions - Acceptable if necessary
  • 6+ conditions - Consider refactoring

4. Order by Likelihood of Failure

Place most likely to fail conditions first:
conditions: [
  createCondition({ /* paused check - most likely to fail */ }),
  createCondition({ /* balance check */ }),
  createCondition({ /* complex calculation - expensive */ })
]

5. Handle Decimal Precision

// ❌ Wrong - USDC has 6 decimals
value: parseUnits('100', 18)

// ✅ Correct
value: parseUnits('100', 6)

6. Use Appropriate Constraint Types

// ✅ Use EQ for booleans
createCondition({
  functionName: 'paused',
  value: 0n, // false
  type: ConditionType.EQ
})

// ✅ Use GTE for minimum thresholds
createCondition({
  functionName: 'balanceOf',
  value: 1000n,
  type: ConditionType.GTE // At least 1000
})

// ✅ Use LTE for maximum limits
createCondition({
  functionName: 'getHealthFactor',
  value: parseUnits('10', 18),
  type: ConditionType.LTE // At most 10.0
})

Troubleshooting

Condition Always Fails

Problem: Condition consistently reverts. Solutions:
  • Verify decimal precision matches token (USDC = 6, WETH = 18)
  • Check constraint type is correct (GTE vs LTE vs EQ)
  • Test the view function directly to see actual return value
  • Ensure target contract address is correct
// ❌ Wrong - USDC uses 6 decimals
value: parseUnits('100', 18)

// ✅ Correct
value: parseUnits('100', 6)

Transaction Stuck in PENDING

Problem: Transaction never executes. Solutions:
  • Check if condition can realistically be met
  • Verify correct contract address
  • Set reasonable upperBoundTimestamp
  • Test condition function returns expected value
// Add reasonable timeout
upperBoundTimestamp: Math.floor(Date.now() / 1000) + 300 // 5 minutes

Type Errors with ABI

Problem: TypeScript complains about function name or args. Solutions:
  • Ensure ABI is defined with as const
  • Check function exists in ABI
  • Verify args match function signature
// ✅ Correct - ABI with 'as const'
const myAbi = [
  {
    inputs: [{ name: 'account', type: 'address' }],
    name: 'balanceOf',
    outputs: [{ name: '', type: 'uint256' }],
    stateMutability: 'view',
    type: 'function'
  }
] as const // ✅ Important!

Gas Estimation Fails

Problem: Transaction simulation fails. Solutions:
  • Simplify conditions or reduce count
  • Verify view functions can be called successfully
  • Check contract addresses are correct
  • Ensure function is truly view/pure (no state changes)

Summary

Conditional execution adds powerful runtime validation to your transactions:
  • ✅ Use createCondition() to define conditions
  • ✅ Add to any buildComposable call via conditions array
  • ✅ All conditions must pass for execution
  • ✅ Transactions wait intelligently until conditions are met
  • ✅ Perfect for limit orders, price triggers, and safe execution
  • ✅ Works with all instruction types
Start with simple balance checks, then expand to limit orders and complex validations.
I