The Universal Action Policy provides granular control over function parameters. Use it when your agent handles funds and you need spending limits, recipient whitelisting, or parameter validation.
When to Use Universal Action
Need per-action spending limits
Need cumulative spending caps
Want to restrict recipients
Need parameter validation
Core Concepts
Parameter Rules
Each rule validates one parameter in the function call:
{
condition: ParamCondition.LESS_THAN_OR_EQUAL, // Comparison type
offsetIndex: 32, // Byte offset in calldata
ref: pad(toHex(parseUnits("500", 6))), // Value to compare against
isLimited: true, // Track cumulative usage?
usage: { limit: parseUnits("5000", 6), used: 0n } // Cumulative limit
}
Conditions
| Condition | Meaning | Use Case |
|---|
EQUAL | Must equal ref | Whitelist addresses |
LESS_THAN | Must be < ref | Amount caps |
LESS_THAN_OR_EQUAL | Must be ≤ ref | Amount caps (inclusive) |
GREATER_THAN | Must be > ref | Minimum amounts |
GREATER_THAN_OR_EQUAL | Must be ≥ ref | Minimum amounts |
NOT_EQUAL | Must not equal ref | Blacklist |
Offset Calculation
Parameters are at 32-byte offsets:
- First parameter:
0
- Second parameter:
32
- Third parameter:
64
- And so on…
Trading Agent Example
Limit per-trade and daily spending:
import { getUniversalActionPolicy, ParamCondition } from "@biconomy/abstractjs";
const UNISWAP_ROUTER = "0x2626664c2603336E57B271c5C0b26F421741e481";
const tradingPolicy = getUniversalActionPolicy({
valueLimitPerUse: 0n, // No ETH value
paramRules: {
length: 2n, // Number of active rules
rules: [
// Rule 1: Recipient must be user's account
{
condition: ParamCondition.EQUAL,
offsetIndex: 96, // recipient parameter offset
ref: pad(userAccountAddress),
isLimited: false,
usage: { limit: 0n, used: 0n }
},
// Rule 2: Max $500 per trade, $5000 daily cumulative
{
condition: ParamCondition.LESS_THAN_OR_EQUAL,
offsetIndex: 128, // amountIn parameter offset
ref: pad(toHex(parseUnits("500", 6))),
isLimited: true, // Track cumulative
usage: {
limit: parseUnits("5000", 6), // $5000 total limit
used: 0n
}
},
// Fill remaining slots with empty rules
...Array(14).fill(EMPTY_RULE)
]
}
});
const sessionDetails = await sessionClient.grantPermissionTypedDataSign({
redeemer: agentSigner.address,
feeToken: { address: USDC, chainId: base.id },
sessionValidUntil: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
actions: [{
chainId: base.id,
actionTarget: UNISWAP_ROUTER,
actionTargetSelector: toFunctionSelector("exactInputSingle(...)"),
actionPolicies: [tradingPolicy]
}]
});
Payment Agent Example
Fixed recipient and amount:
const MERCHANT = "0x1234...";
const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const paymentPolicy = getUniversalActionPolicy({
valueLimitPerUse: 0n,
paramRules: {
length: 2n,
rules: [
// Only send to merchant
{
condition: ParamCondition.EQUAL,
offsetIndex: 0, // First param: recipient
ref: pad(MERCHANT),
isLimited: false,
usage: { limit: 0n, used: 0n }
},
// Max 100 USDC per payment
{
condition: ParamCondition.LESS_THAN_OR_EQUAL,
offsetIndex: 32, // Second param: amount
ref: pad(toHex(parseUnits("100", 6))),
isLimited: false,
usage: { limit: 0n, used: 0n }
},
...Array(14).fill(EMPTY_RULE)
]
}
});
const sessionDetails = await sessionClient.grantPermissionTypedDataSign({
redeemer: agentSigner.address,
feeToken: { address: USDC, chainId: base.id },
sessionValidUntil: Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60,
actions: [{
chainId: base.id,
actionTarget: USDC,
actionTargetSelector: toFunctionSelector("transfer(address,uint256)"),
usageLimit: 12n, // 12 payments max (monthly for 1 year)
actionPolicies: [paymentPolicy]
}]
});
Rebalancing Agent Example
Limit rebalance size to % of portfolio:
const rebalancePolicy = getUniversalActionPolicy({
valueLimitPerUse: 0n,
paramRules: {
length: 1n,
rules: [
// Max 10% of portfolio per rebalance ($10,000 cap)
{
condition: ParamCondition.LESS_THAN_OR_EQUAL,
offsetIndex: 128, // amountIn
ref: pad(toHex(parseUnits("10000", 6))),
isLimited: true,
usage: {
limit: parseUnits("50000", 6), // $50k total per period
used: 0n
}
},
...Array(15).fill(EMPTY_RULE)
]
}
});
Empty Rule Template
Fill unused rule slots with this:
const EMPTY_RULE = {
condition: ParamCondition.EQUAL,
offsetIndex: 0,
isLimited: false,
ref: "0x0000000000000000000000000000000000000000000000000000000000000000",
usage: { limit: 0n, used: 0n }
};
Per-Action vs Cumulative Limits
Per-Action Only
Each individual action must be ≤ limit, but no cumulative tracking:
{
condition: ParamCondition.LESS_THAN_OR_EQUAL,
offsetIndex: 32,
ref: pad(toHex(parseUnits("500", 6))), // Max $500 per trade
isLimited: false, // No cumulative tracking
usage: { limit: 0n, used: 0n }
}
Cumulative Tracking
Track total spent across all actions:
{
condition: ParamCondition.LESS_THAN_OR_EQUAL,
offsetIndex: 32,
ref: pad(toHex(parseUnits("500", 6))), // Max $500 per trade
isLimited: true, // Track cumulative
usage: {
limit: parseUnits("5000", 6), // $5000 total
used: 0n
}
}
Multiple Rules Example
Validate multiple parameters:
// For transfer(address recipient, uint256 amount)
const policy = getUniversalActionPolicy({
valueLimitPerUse: 0n,
paramRules: {
length: 3n,
rules: [
// Rule 1: Recipient in whitelist (address A)
{
condition: ParamCondition.EQUAL,
offsetIndex: 0,
ref: pad(ALLOWED_RECIPIENT_A),
isLimited: false,
usage: { limit: 0n, used: 0n }
},
// Rule 2: OR recipient B (rules are OR'd for same offset)
{
condition: ParamCondition.EQUAL,
offsetIndex: 0,
ref: pad(ALLOWED_RECIPIENT_B),
isLimited: false,
usage: { limit: 0n, used: 0n }
},
// Rule 3: Amount limit
{
condition: ParamCondition.LESS_THAN_OR_EQUAL,
offsetIndex: 32,
ref: pad(toHex(parseUnits("1000", 6))),
isLimited: true,
usage: { limit: parseUnits("10000", 6), used: 0n }
},
...Array(13).fill(EMPTY_RULE)
]
}
});
Finding Parameter Offsets
For a function like swap(address tokenIn, address tokenOut, uint256 amount):
Offset 0: tokenIn (address)
Offset 32: tokenOut (address)
Offset 64: amount (uint256)
For structs, offsets are more complex. Check the ABI encoding.
Common Patterns
Whitelist Single Recipient
{
condition: ParamCondition.EQUAL,
offsetIndex: 0, // recipient param
ref: pad(ALLOWED_ADDRESS),
isLimited: false,
usage: { limit: 0n, used: 0n }
}
Cap Per-Transaction Amount
{
condition: ParamCondition.LESS_THAN_OR_EQUAL,
offsetIndex: 32, // amount param
ref: pad(toHex(parseUnits("500", 6))),
isLimited: false,
usage: { limit: 0n, used: 0n }
}
Daily/Total Spending Limit
{
condition: ParamCondition.LESS_THAN_OR_EQUAL,
offsetIndex: 32,
ref: pad(toHex(parseUnits("500", 6))), // Per-tx max
isLimited: true,
usage: {
limit: parseUnits("5000", 6), // Cumulative max
used: 0n
}
}
Minimum Amount
{
condition: ParamCondition.GREATER_THAN_OR_EQUAL,
offsetIndex: 32,
ref: pad(toHex(parseUnits("10", 6))), // At least $10
isLimited: false,
usage: { limit: 0n, used: 0n }
}
Debugging Tips
- Check offsets: Use
console.log(encodeFunctionData(...)) to see actual calldata
- Pad values correctly: Always use
pad() for addresses and values
- Match decimals: USDC = 6, ETH = 18, etc.
- Test on testnet: Verify rules work before mainnet