Policies
Policies are rule-based controls that govern transaction execution. They can allow, deny, or require approval for intents based on flexible criteria.
Policy Structure
interface Policy {
id: string; // UUID policy identifier
walletId: string; // Associated wallet UUID
name: string; // Human-readable name
version: number; // Incremental version number
active: boolean; // Policy enforcement toggle
rules: PolicyRule[]; // Array of rule objects
createdAt: string; // ISO-8601 creation timestamp
updatedAt: string; // ISO-8601 update timestamp
}
Policy Rules
Agentic Wallet supports 11 rule types:
Spending Limits
interface SpendingLimitRule {
type: 'spending_limit';
maxLamportsPerTx?: number; // Per-transaction limit
maxLamportsPerDay?: number; // Daily spending cap
requireApprovalAboveLamports?: number; // Approval threshold
}
Example:
{
"type": "spending_limit",
"maxLamportsPerTx": 1000000000,
"maxLamportsPerDay": 10000000000,
"requireApprovalAboveLamports": 500000000
}
Behavior:
maxLamportsPerTx: Hard limit, transaction denied if exceeded
maxLamportsPerDay: Cumulative daily limit, resets at UTC midnight
requireApprovalAboveLamports: Moves to approval_gate status if exceeded
Address Controls
Address Allowlist
Only allow transfers to specific addresses.{
"type": "address_allowlist",
"addresses": [
"9B5XszUGdMaxCZ7uSQhPzdks5ZQSmWxrmzCSvtJ6Ns6g",
"AnotherValidAddress123..."
]
}
Address Blocklist
Deny transfers to specific addresses.{
"type": "address_blocklist",
"addresses": [
"KnownScamAddress123...",
"BlockedAddress456..."
]
}
Program and Token Controls
Program Allowlist
Restrict which Solana programs can be invoked.{
"type": "program_allowlist",
"programIds": [
"11111111111111111111111111111111",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
]
}
Token Allowlist
Restrict token transfers to specific mints.{
"type": "token_allowlist",
"mints": [
"So11111111111111111111111111111111111111112",
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
]
}
Protocol Allowlist
{
"type": "protocol_allowlist",
"protocols": ["system-program", "jupiter", "marinade"]
}
Restricts execution to specific protocol adapters.
Rate Limiting
interface RateLimitRule {
type: 'rate_limit';
maxTx: number; // Maximum transactions
windowSeconds: number; // Time window
}
Example:
{
"type": "rate_limit",
"maxTx": 10,
"windowSeconds": 60
}
Limits to 10 transactions per 60-second rolling window.
Time Windows
{
"type": "time_window",
"startHourUtc": 9,
"endHourUtc": 17
}
Only allow transactions during UTC business hours (9 AM - 5 PM).
Slippage Control
{
"type": "max_slippage",
"maxBps": 100
}
Reject swaps with slippage exceeding 100 basis points (1%).
Protocol Risk Rules
interface ProtocolRiskRule {
type: 'protocol_risk';
protocol: string;
maxSlippageBps?: number;
maxPoolConcentrationBps?: number;
allowedPools?: string[];
allowedPrograms?: string[];
oracleDeviationBps?: number;
}
Example:
{
"type": "protocol_risk",
"protocol": "jupiter",
"maxSlippageBps": 200,
"oracleDeviationBps": 500,
"allowedPrograms": [
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"
]
}
Checks:
- maxSlippageBps: Protocol-specific slippage limit
- maxPoolConcentrationBps: Reject if pool is too concentrated
- allowedPools: Whitelist specific liquidity pools
- allowedPrograms: Restrict program IDs for this protocol
- oracleDeviationBps: Compare quoted price vs oracle, require approval if deviation exceeds threshold
Portfolio Risk Rules
interface PortfolioRiskRule {
type: 'portfolio_risk';
maxDrawdownLamports?: number;
maxDailyLossLamports?: number;
maxExposureBpsPerToken?: number;
maxExposureBpsPerProtocol?: number;
}
Example:
{
"type": "portfolio_risk",
"maxDailyLossLamports": 2000000000,
"maxExposureBpsPerProtocol": 6000
}
Checks:
- maxDailyLossLamports: Deny if projected loss exceeds limit
- maxDrawdownLamports: Deny if drawdown from high-water mark exceeds limit
- maxExposureBpsPerToken: Require approval if token concentration > limit (in bps)
- maxExposureBpsPerProtocol: Require approval if protocol exposure > limit
Portfolio risk checks require the caller to compute projected values and pass them in the evaluation request.
Policy Evaluation
Policies are evaluated before transaction execution:
interface PolicyEvaluationRequest {
walletId: string;
agentId?: string;
type: string; // Transaction type
protocol: string;
destination?: string;
tokenMint?: string;
amountLamports?: number;
programIds?: string[];
slippageBps?: number;
pool?: string;
poolConcentrationBps?: number;
oraclePriceUsd?: number;
quotedPriceUsd?: number;
projectedDailyLossLamports?: number;
projectedDrawdownLamports?: number;
projectedTokenExposureBps?: number;
projectedProtocolExposureBps?: number;
timestamp?: string;
riskTierHint?: 'low' | 'medium' | 'high' | 'critical';
}
Evaluation Response
interface PolicyDecision {
decision: 'allow' | 'deny' | 'require_approval';
reasons: string[]; // Human-readable explanations
riskTier: 'low' | 'medium' | 'high' | 'critical';
}
Example:
{
"decision": "deny",
"reasons": [
"Amount 2000000000 exceeds maxLamportsPerTx 1000000000",
"Protocol opensea not allowed"
],
"riskTier": "high"
}
Evaluation Flow
Risk Tier Deduction
// services/policy-engine/src/engine/policy-evaluator.ts:21
const RESTRICTED_TYPES = new Set([
'flash_loan_bundle',
'cpi_call',
'custom_instruction_bundle'
]);
function deduceRiskTier(input: PolicyEvaluationRequest): RiskTier {
if (input.riskTierHint) return input.riskTierHint;
if (RESTRICTED_TYPES.has(input.type)) return 'critical';
if ((input.amountLamports ?? 0) > 2_000_000_000) return 'high';
if (input.type.includes('swap') ||
input.type.includes('lend') ||
input.type.includes('stake')) return 'medium';
return 'low';
}
Critical tier automatically triggers require_approval.
Policy Versioning
Policies use incremental versioning:
// On update:
policy.version = policy.version + 1;
policy.updatedAt = new Date().toISOString();
Listing Versions
npm run cli -- policy versions <policyId>
Returns all historical versions of a policy.
Retrieving Specific Version
npm run cli -- policy version <policyId> --number 3
Returns version 3 of the policy.
Policy Migration
Migrate policies to new versions with transformations:
npm run cli -- policy migrate <policyId> \
--target-version 5 \
--mode add_default_risk_rules
Migration Modes
Validate and normalize existing rules without adding new ones.
Add default protocol_risk and portfolio_risk rules if missing:// services/policy-engine/src/index.ts:150
if (!hasProtocolRisk) {
nextRules.push({
type: 'protocol_risk',
protocol: 'jupiter',
maxSlippageBps: 200,
oracleDeviationBps: 500,
});
}
if (!hasPortfolioRisk) {
nextRules.push({
type: 'portfolio_risk',
maxDailyLossLamports: 2_000_000_000,
maxExposureBpsPerProtocol: 6000,
});
}
Approval Gates
When a policy returns require_approval, the transaction enters approval_gate status:
status: 'approval_gate'
reasons: [
"Amount 600000000 is above approval threshold 500000000"
]
Manual Approval
npm run cli -- tx approve <txId>
Approving resumes the pipeline from the approval gate. Rejecting moves the transaction to failed status.
Pending Approvals
List all transactions awaiting approval:
npm run cli -- tx pending --wallet-id <walletId>
Compatibility Check
Validate rule compatibility before creating policies:
npm run cli -- policy compatibility-check \
--rules '[{"type":"spending_limit","maxLamportsPerTx":1000000000}]'
Response:
{
"data": {
"compatible": true,
"supportedRuleTypes": [
"spending_limit",
"address_allowlist",
"address_blocklist",
"program_allowlist",
"token_allowlist",
"protocol_allowlist",
"rate_limit",
"time_window",
"max_slippage",
"protocol_risk",
"portfolio_risk"
],
"unsupportedRuleTypes": []
}
}
Policy State Management
The policy evaluator maintains state for stateful rules:
// services/policy-engine/src/engine/policy-evaluator.ts:38
class PolicyEvaluator {
private readonly rateLimitEvents = new Map<string, RateLimitWindow[]>();
private readonly dailySpendByWallet = new Map<string, number>();
private readonly snapshotFile: string | undefined;
constructor(snapshotFile?: string) {
// Load state from disk
const snapshot = readJsonFile<PolicyEvaluatorSnapshot>(this.snapshotFile);
// ...
}
private persist(): void {
// Save state to disk after each evaluation
writeJsonFile(this.snapshotFile, {
rateLimitEvents: [...this.rateLimitEvents.entries()],
dailySpendByWallet: [...this.dailySpendByWallet.entries()],
});
}
}
Persisted state:
- rateLimitEvents: Rolling window of transaction timestamps per wallet/agent
- dailySpendByWallet: Cumulative spending per wallet per day (keyed by
walletId:YYYY-MM-DD)
Best Practices
- Always set spending limits for autonomous wallets
- Use address allowlists for high-value recipient addresses
- Combine multiple rule types for defense in depth
- Set rate limits to prevent runaway execution
- Use time windows for scheduled operations
- Enable protocol_risk rules for DeFi operations
- Monitor approval queue length
- Start with strict policies and relax as needed
- Use
requireApprovalAboveLamports for human oversight
- Test policies with paper trades before live execution
- Version policies on every change for audit trail
- Document policy intent in the
name field
- Combine protocol_allowlist with protocol_risk for granular control
- Never disable all policies on production wallets
- Use program_allowlist to prevent malicious contract calls
- Set conservative oracle deviation thresholds
- Implement portfolio exposure limits
- Require approval for critical risk tier operations
- Audit policy changes and maintain version history
Source Code Reference
Policy functionality is implemented in:
services/policy-engine/src/engine/policy-evaluator.ts - Evaluation engine (services/policy-engine/src/engine/policy-evaluator.ts:1)
services/policy-engine/src/index.ts - Policy API (services/policy-engine/src/index.ts:1)
packages/common/src/schemas/policy.ts - Rule type definitions (packages/common/src/schemas/policy.ts:1)