Skip to main content

Overview

Sardis implements a fail-closed policy firewall that validates every payment before it reaches the MPC signing layer. This prevents financial hallucinations, off-task spending, and policy violations—even if an AI agent is compromised or behaves unexpectedly.
Fail-Closed Design: If any policy check fails or throws an exception, the transaction is automatically denied. No payment can bypass policy enforcement.

Policy Architecture

Every AI agent has a SpendingPolicy that defines:
  • Amount limits: Per-transaction, daily, weekly, monthly, and lifetime caps
  • Merchant controls: Allowlists, blocklists, per-merchant caps
  • Scope restrictions: Limit spending to specific categories (compute, retail, etc.)
  • MCC blocking: Block entire merchant category codes (gambling, adult content)
  • Goal drift detection: Prevent off-task spending
  • Approval routing: Auto-approve small payments, require human sign-off for large ones

Evaluation Pipeline

Location: packages/sardis-core/src/sardis_v2_core/spending_policy.py:246-417 Every payment goes through a 10-step check pipeline. The first failure short-circuits with a denial reason:

1. Amount Validation

if amount <= 0:
    return False, "amount_must_be_positive"
if fee < 0:
    return False, "fee_must_be_non_negative"

total_cost = amount + fee  # All limits include gas fees

2. Scope Check

if SpendingScope.ALL not in self.allowed_scopes and scope not in self.allowed_scopes:
    return False, "scope_not_allowed"
Supported Scopes:
  • ALL — No restrictions
  • RETAIL — Physical goods, groceries, etc.
  • DIGITAL — Software, subscriptions
  • SERVICES — Professional services
  • COMPUTE — Cloud compute, API credits
  • DATA — Data purchases, feeds
  • AGENT_TO_AGENT — Inter-agent payments

3. MCC Check

Block entire merchant categories by 4-digit MCC code:
if mcc_code:
    mcc_ok, mcc_reason = self._check_mcc_policy(mcc_code)
    if not mcc_ok:
        return False, mcc_reason
Location: spending_policy.py:606-627 High-Risk MCCs (Blocked by Default):
  • 7995 — Gambling
  • 5967 — Adult content
  • 6012 — Payday loans
  • 5993 — Tobacco

4. Per-Transaction Limit

effective_per_tx = self._get_effective_per_tx_limit(mcc_code, merchant_category)
if total_cost > effective_per_tx:
    return False, "per_transaction_limit"
Category-Specific Overrides: You can set different limits per category:
policy.add_merchant_allow(
    category="cloud",
    max_per_tx=Decimal("500.00"),  # Allow up to $500 for cloud services
)
policy.add_merchant_allow(
    category="groceries",
    max_per_tx=Decimal("100.00"),  # Cap groceries at $100
)
Location: spending_policy.py:560-586

5. Total Limit

if self.spent_total + total_cost > self.limit_total:
    return False, "total_limit_exceeded"
Cumulative spend is tracked in the database to prevent race conditions.

6. Time-Window Limits

Rolling daily/weekly/monthly caps:
for window_limit in [self.daily_limit, self.weekly_limit, self.monthly_limit]:
    ok, reason = window_limit.can_spend(total_cost)
    if not ok:
        return ok, reason  # "daily_limit_exceeded", etc.
Location: spending_policy.py:96-141 Auto-Reset: Windows reset automatically when they expire:
def reset_if_expired(self) -> bool:
    now = datetime.now(timezone.utc)
    if self.window_type == "daily":
        duration = timedelta(days=1)
    elif self.window_type == "weekly":
        duration = timedelta(weeks=1)
    elif self.window_type == "monthly":
        duration = timedelta(days=30)
    else:
        return False
    if now >= self.window_start + duration:
        self.current_spent = Decimal("0")
        self.window_start = now
        return True
    return False

7. On-Chain Balance Check

if rpc_client:
    balance = await wallet.get_balance(chain, token, rpc_client)
    if balance < total_cost:
        return False, "insufficient_balance"
Sardis is non-custodial—the agent’s actual blockchain balance is the source of truth.

8. Merchant Rules

Deny Rules (checked first):
for rule in self.merchant_rules:
    if rule.rule_type == "deny" and rule.matches_merchant(merchant_id, merchant_category):
        return False, "merchant_denied"
Allow Rules (allowlist enforcement):
allow_rules = [rule for rule in self.merchant_rules if rule.rule_type == "allow"]
if allow_rules:
    match = next((r for r in allow_rules if r.matches_merchant(merchant_id, merchant_category)), None)
    if not match:
        return False, "merchant_not_allowlisted"
    if match.max_per_tx and amount > match.max_per_tx:
        return False, "merchant_cap_exceeded"
Location: spending_policy.py:588-604

9. Goal Drift Detection

if drift_score is not None and self.max_drift_score is not None:
    if drift_score > self.max_drift_score:
        return False, "goal_drift_exceeded"
Drift Scoring: Measures how far the agent has deviated from its stated objective. Requires integration with an external drift detection service.

10. Approval Threshold

if self.approval_threshold is not None and amount > self.approval_threshold:
    return True, "requires_approval"
Approved transactions that exceed the threshold return (True, "requires_approval") and are routed to a human for sign-off.

11. KYA Attestation (MEDIUM/HIGH Trust)

For MEDIUM and HIGH trust agents, verify on-chain KYA attestation:
if kya_client and self.trust_level in (TrustLevel.MEDIUM, TrustLevel.HIGH):
    kya_ok, kya_reason = await self._check_kya_attestation(wallet, kya_client)
    if not kya_ok:
        return False, kya_reason
Location: spending_policy.py:629-664

Trust Levels

Sardis uses trust levels to determine default spending limits:
Trust LevelPer-TxDailyWeeklyMonthlyTotal
LOW$50$100$500$1,000$5,000
MEDIUM$500$1,000$5,000$10,000$50,000
HIGH$5,000$10,000$50,000$100,000$500,000
UNLIMITEDNo capNo capNo capNo capNo cap
Location: spending_policy.py:724-742 Default Policy Creation:
from sardis_v2_core import create_default_policy, TrustLevel

policy = create_default_policy(
    agent_id="agent_123",
    trust_level=TrustLevel.LOW,
)

Real-Time Validation

Policies are evaluated synchronously before every transaction:
from sardis_wallet import EnhancedWalletManager

manager = EnhancedWalletManager(settings, async_policy_store=policy_store)

# Async policy check (production)
evaluation = await manager.async_validate_policies(mandate)
if not evaluation.allowed:
    raise HTTPException(403, detail=evaluation.reason)

# Comprehensive check (with balance, sessions, multi-sig)
evaluation = await manager.evaluate_policies(
    wallet=wallet,
    mandate=mandate,
    chain="base",
    token=TokenType.USDC,
    rpc_client=rpc_client,
    session_id=session_id,
)
Location: packages/sardis-wallet/src/sardis_wallet/manager.py:223-427

Financial Hallucination Prevention

Deterministic Validation

All policy checks are deterministic—no LLM involvement in enforcement

Execution Context Validation

Chain, token, and destination address are validated before signing

Fail-Closed Design

Any exception or error defaults to transaction denial

Immutable Audit Trail

All denials are logged with reason codes for forensic analysis

Execution Context Validation

Even if an agent passes the spending policy, Sardis validates the execution context:
ok, reason = policy.validate_execution_context(
    destination="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
    chain="base",
    token="USDC",
)
Location: spending_policy.py:431-471 Checks:
  • Chain allowlist: Only permit specific chains
  • Token allowlist: Only permit specific tokens (e.g., stablecoins only)
  • Destination allowlist: Restrict payments to approved addresses
  • Destination blocklist: Block known malicious addresses

Spending Policy Examples

Example 1: Low-Trust Agent (Default)

from sardis_v2_core import create_default_policy, TrustLevel

policy = create_default_policy("agent_001", TrustLevel.LOW)
print(policy.limit_per_tx)  # Decimal('50.00')
print(policy.daily_limit.limit_amount)  # Decimal('100.00')

Example 2: Cloud-Only Agent

from sardis_v2_core import SpendingPolicy, SpendingScope, TrustLevel
from decimal import Decimal

policy = SpendingPolicy(
    agent_id="agent_cloud_001",
    trust_level=TrustLevel.MEDIUM,
    limit_per_tx=Decimal("500.00"),
    limit_total=Decimal("10000.00"),
    allowed_scopes=[SpendingScope.COMPUTE],  # Only cloud services
    blocked_merchant_categories=["gambling", "adult"],
)

# Add allowlist for specific providers
policy.add_merchant_allow(merchant_id="aws.amazon.com", max_per_tx=Decimal("1000.00"))
policy.add_merchant_allow(merchant_id="cloud.google.com", max_per_tx=Decimal("1000.00"))
policy.add_merchant_allow(merchant_id="azure.microsoft.com", max_per_tx=Decimal("1000.00"))

Example 3: Retail Agent with Category Caps

policy = SpendingPolicy(
    agent_id="agent_retail_001",
    trust_level=TrustLevel.LOW,
    limit_per_tx=Decimal("200.00"),
    limit_total=Decimal("5000.00"),
    allowed_scopes=[SpendingScope.RETAIL],
)

# Different caps per category
policy.add_merchant_allow(category="groceries", max_per_tx=Decimal("100.00"))
policy.add_merchant_allow(category="restaurants", max_per_tx=Decimal("50.00"))
policy.add_merchant_allow(category="gas_stations", max_per_tx=Decimal("75.00"))

# Block high-risk categories
policy.block_merchant_category("alcohol")
policy.block_merchant_category("tobacco")

Example 4: Stablecoin-Only Agent

policy = SpendingPolicy(
    agent_id="agent_stable_001",
    trust_level=TrustLevel.HIGH,
    limit_per_tx=Decimal("5000.00"),
    limit_total=Decimal("500000.00"),
    allowed_chains=["base", "polygon", "arbitrum"],
    allowed_tokens=["USDC", "USDT", "PYUSD"],  # Stablecoins only
)

Database-Backed Enforcement

In production, cumulative spend is tracked in PostgreSQL to prevent race conditions:
# In-memory (dev/test)
policy.spent_total += amount

# Database-backed (production)
db_state = await policy_store.load_state(agent_id)
if db_state["spent_total"] + total_cost > policy.limit_total:
    return False, "total_limit_exceeded"
Location: spending_policy.py:343-374

Velocity Checks

Prevent rapid-fire transactions:
vel_ok, vel_reason = await policy_store.check_velocity(agent_id)
if not vel_ok:
    return False, vel_reason  # "too_many_requests"

Audit Logging

Every policy decision is logged:
await audit_logger.log(
    wallet_id=wallet.wallet_id,
    category=AuditCategory.POLICY,
    action=AuditAction.TRANSACTION_DENIED,
    actor_id=agent_id,
    resource_type="transaction",
    resource_id=tx_hash,
    details={"reason": reason, "amount": str(amount), "merchant": merchant_id},
)
Location: packages/sardis-wallet/src/sardis_wallet/audit_log.py

Production Checklist

1

Define Trust Levels

Start agents at LOW trust, upgrade to MEDIUM/HIGH as they prove reliable
2

Set Scope Restrictions

Use allowed_scopes to limit agents to specific spending categories
3

Configure Merchant Rules

Set up allowlists for known-good merchants, blocklists for high-risk categories
4

Enable Database Tracking

Use AsyncPolicyStore for race-safe cumulative spend tracking
5

Monitor Policy Denials

Set up alerts for high denial rates—may indicate agent misbehavior
6

Test Fail-Closed Behavior

Verify that policy check failures always result in transaction denial

Next Steps

MPC Architecture

Learn how MPC signing prevents private key exposure

Best Practices

Security hardening, API key management, and monitoring

Build docs developers (and LLMs) love