Skip to main content
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

Agent handles user funds
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

ConditionMeaningUse Case
EQUALMust equal refWhitelist addresses
LESS_THANMust be < refAmount caps
LESS_THAN_OR_EQUALMust be ≤ refAmount caps (inclusive)
GREATER_THANMust be > refMinimum amounts
GREATER_THAN_OR_EQUALMust be ≥ refMinimum amounts
NOT_EQUALMust not equal refBlacklist

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

  1. Check offsets: Use console.log(encodeFunctionData(...)) to see actual calldata
  2. Pad values correctly: Always use pad() for addresses and values
  3. Match decimals: USDC = 6, ETH = 18, etc.
  4. Test on testnet: Verify rules work before mainnet