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
Install the SDK
npm install @biconomy/abstractjs@latest viem
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'
})
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
}
})
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:
Immediate execution - All conditions pass → Transaction executes immediately
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 : 2 n
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: 0 n , // 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: 0 n ,
type: ConditionType . EQ
})
]
}
})
All conditions must pass - The transaction waits (PENDING) until all conditions are satisfied, or fails if timeout expires.
Limit Orders (Wait for Price)
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 * 101 n / 100 n // 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:
Submit transaction with conditions
PENDING - MEE periodically checks conditions
Condition satisfied - Transaction executes automatically
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: 0 n , // false
type: ConditionType . EQ
})
// ✅ Use GTE for minimum thresholds
createCondition ({
functionName: 'balanceOf' ,
value: 1000 n ,
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.