Skip to main content
Vincent Policies are programmable guardrails built using Lit Actions that determine whether Vincent Apps can execute specific Abilities on behalf of users. They ensure autonomous agents operate strictly within user-defined boundaries.

Policy Lifecycle

Vincent Policies execute in two phases during Ability execution:
1

Precheck

Runs locally to validate that execution will likely succeed. Provides best-effort validation before network resources are used
2

Evaluate

Runs in the Lit Action environment with full blockchain access to make the final allow/deny decision
Only if all policies return allow results will the Ability’s execute function proceed. After successful execution, the Ability may call the Policy’s commit function to update state.

Defining Your Policy

packageName
string
required
The npm package name of your Policy (e.g., @your-org/policy-name)
abilityParamsSchema
ZodSchema
required
Zod schema defining what parameters Abilities must provide to your Policy
userParamsSchema
ZodSchema
required
Zod schema for user-configurable boundaries stored securely on-chain
precheckAllowResultSchema
ZodSchema
Zod schema for successful precheck return values
precheckDenyResultSchema
ZodSchema
Zod schema for denied precheck return values
precheck
function
required
Async function providing fast, local validation before using network resources
evalAllowResultSchema
ZodSchema
Zod schema for successful evaluation return values
evalDenyResultSchema
ZodSchema
Zod schema for denied evaluation return values
evaluate
function
required
Async function with full blockchain access making final allow/deny decision
commitParamsSchema
ZodSchema
Zod schema for parameters passed to commit function
commitAllowResultSchema
ZodSchema
Zod schema for successful commit return values
commitDenyResultSchema
ZodSchema
Zod schema for failed commit return values
commit
function
Optional async function to update policy state after successful Ability execution

Creating Vincent Policies

Vincent Policies are created using createVincentPolicy from the Vincent Ability SDK:
import { createVincentPolicy } from '@lit-protocol/vincent-ability-sdk';

export const vincentPolicy = createVincentPolicy({
  packageName: '@your-org/policy-name',

  abilityParamsSchema,
  userParamsSchema,

  precheckAllowResultSchema,
  precheckDenyResultSchema,
  precheck: async ({ abilityParams, userParams }, policyContext) => {
    // Local validation logic
  },

  evalAllowResultSchema,
  evalDenyResultSchema,
  evaluate: async ({ abilityParams, userParams }, policyContext) => {
    // Final validation with blockchain access
  },

  commitParamsSchema,
  commitAllowResultSchema,
  commitDenyResultSchema,
  commit: async (params, policyContext) => {
    // Update policy state after successful execution
  },
});

Example Implementation

import { createVincentPolicy } from '@lit-protocol/vincent-ability-sdk';
import { z } from 'zod';

const vincentPolicy = createVincentPolicy({
  packageName: '@example/spending-limit-policy',

  abilityParamsSchema: z.object({
    tokenAddress: z.string(),
    amount: z.number(),
  }),

  userParamsSchema: z.object({
    dailySpendingLimit: z.number(),
    allowedTokens: z.array(z.string()).optional(),
  }),

  precheckAllowResultSchema: z.object({
    maxDailySpendingLimit: z.number(),
    currentDailySpending: z.number(),
    allowedTokens: z.array(z.string()),
  }),

  precheckDenyResultSchema: z.object({
    reason: z.string(),
    maxDailySpendingLimit: z.number(),
    currentDailySpending: z.number(),
    allowedTokens: z.array(z.string()),
  }),

  precheck: async ({ abilityParams, userParams }, policyContext) => {
    const { amount, tokenAddress } = abilityParams;
    const { dailySpendingLimit, allowedTokens } = userParams;

    const isTokenAllowed = allowedTokens?.includes(tokenAddress) ?? true;
    const { isSpendingLimitExceeded, currentDailySpending } = await checkSpendingLimit(
      tokenAddress,
      amount,
      dailySpendingLimit,
    );

    if (!isTokenAllowed) {
      return policyContext.deny({
        reason: 'Token not allowed',
        maxDailySpendingLimit: dailySpendingLimit,
        currentDailySpending,
        allowedTokens: allowedTokens || [],
      });
    }

    if (isSpendingLimitExceeded) {
      return policyContext.deny({
        reason: 'Spending limit exceeded',
        maxDailySpendingLimit: dailySpendingLimit,
        currentDailySpending,
        allowedTokens: allowedTokens || [],
      });
    }

    return policyContext.allow({
      maxDailySpendingLimit: dailySpendingLimit,
      currentDailySpending,
      allowedTokens: allowedTokens || [],
    });
  },

  evalAllowResultSchema: z.object({
    maxDailySpendingLimit: z.number(),
    currentDailySpending: z.number(),
    allowedTokens: z.array(z.string()),
  }),

  evalDenyResultSchema: z.object({
    reason: z.string(),
    maxDailySpendingLimit: z.number(),
    currentDailySpending: z.number(),
    allowedTokens: z.array(z.string()),
  }),

  evaluate: async ({ abilityParams, userParams }, policyContext) => {
    // Similar to precheck but with blockchain access
    const { amount, tokenAddress } = abilityParams;
    const { dailySpendingLimit, allowedTokens } = userParams;

    // Can access on-chain data here
    const { isSpendingLimitExceeded, currentDailySpending } = await checkOnChainSpendingLimit(
      policyContext.delegation.delegatorPkpInfo.ethAddress,
      tokenAddress,
      amount,
      dailySpendingLimit,
    );

    if (isSpendingLimitExceeded) {
      return policyContext.deny({
        reason: 'Spending limit exceeded',
        maxDailySpendingLimit: dailySpendingLimit,
        currentDailySpending,
        allowedTokens: allowedTokens || [],
      });
    }

    return policyContext.allow({
      maxDailySpendingLimit: dailySpendingLimit,
      currentDailySpending,
      allowedTokens: allowedTokens || [],
    });
  },

  commitParamsSchema: z.object({
    spentAmount: z.number(),
    tokenAddress: z.string(),
  }),

  commitAllowResultSchema: z.object({
    updatedDailySpending: z.number(),
    remainingDailyLimit: z.number(),
  }),

  commitDenyResultSchema: z.object({
    reason: z.string(),
    vincentAppId: z.number(),
    spenderAddress: z.string(),
    spentAmount: z.number(),
    spentTokenAddress: z.string(),
  }),

  commit: async (params, policyContext) => {
    const { spentAmount, tokenAddress } = params;

    try {
      const { updatedDailySpending, remainingDailyLimit } = await updateSpentAmount({
        vincentAppId: policyContext.appId,
        spenderAddress: policyContext.delegation.delegatorPkpInfo.ethAddress,
        spentAmount,
        spentTokenAddress: tokenAddress,
      });

      return policyContext.allow({
        updatedDailySpending,
        remainingDailyLimit,
      });
    } catch (error) {
      return policyContext.deny({
        reason: 'Failed to update spending limit',
        vincentAppId: policyContext.appId,
        spenderAddress: policyContext.delegation.delegatorPkpInfo.ethAddress,
        spentAmount,
        spentTokenAddress: tokenAddress,
      });
    }
  },
});

Deep Dive Guides

I