Skip to main content

What Are Spending Policies?

Spending policies let wallet owners set dollar-denominated caps on what a session key can spend. The CHIPI wallet contract enforces these limits automatically during transaction execution — no backend or middleware needed. Limits are enforced on ERC-20 operations: transfer, approve, increase_allowance.
Spending policies require CHIPI v33 wallets. v29 wallets do not have these entrypoints. See Wallet Upgrades to upgrade.

When to Use Spending Policies

Use CaseConfiguration
AI agent budgetsMax 0.01perAPIcall,0.01 per API call, 50 per day
Game daily limitsMax 5 USDC per trade, 100 USDC per 24h
Employee walletsMax 200 USDC per transaction, 1000 per week
DeFi automationMax position size per swap, rolling window cap

How It Works

1. Owner creates a session key and registers it on-chain
2. Owner sets a spending policy: per-call limit + rolling window limit
3. Session key executes transactions freely within limits
4. Contract enforces automatically — reverts if limits exceeded
5. Window resets automatically after the configured duration
The policy is per (session key, token) pair. You can set different limits for different tokens on the same session.

Configuration

A spending policy has three parameters:
ParameterTypeDescription
maxPerCallbigint (u256)Maximum token amount per single transaction
maxPerWindowbigint (u256)Maximum cumulative amount within the rolling window
windowSecondsnumber (u64)Rolling window duration in seconds
The contract also tracks:
FieldDescription
spentInWindowAmount spent in the current active window
windowStartUnix timestamp when the current window started
If no policy is set (maxPerWindow is 0), the session has no spending limits. All ERC-20 operations are allowed without caps.

TypeScript Backend

import { ChipiServerSDK } from "@chipi-stack/backend";

const sdk = new ChipiServerSDK({
  apiPublicKey: process.env.CHIPI_PUBLIC_KEY!,
  apiSecretKey: process.env.CHIPI_SECRET_KEY!,
});

// USDC on Starknet mainnet (6 decimals)
const USDC = "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb";

// Set policy: max 1 USDC per call, 50 USDC per day
const txHash = await sdk.sessions.setSpendingPolicy({
  encryptKey: userEncryptKey,
  wallet: userWallet,
  spendingPolicyConfig: {
    sessionPublicKey: session.publicKey,
    token: USDC,
    maxPerCall: 1_000_000n,      // 1 USDC (6 decimals)
    maxPerWindow: 50_000_000n,   // 50 USDC
    windowSeconds: 86400,        // 24 hours
  },
}, bearerToken);

// Query current policy and spend
const policy = await sdk.sessions.getSpendingPolicy({
  walletAddress: userWallet.publicKey,
  sessionPublicKey: session.publicKey,
  token: USDC,
});
console.log(`Spent ${policy.spentInWindow} of ${policy.maxPerWindow} this window`);

// Remove policy (session has no limits for this token)
const removeTxHash = await sdk.sessions.removeSpendingPolicy({
  encryptKey: userEncryptKey,
  wallet: userWallet,
  sessionPublicKey: session.publicKey,
  token: USDC,
}, bearerToken);

React / Next.js

The useChipiSession hook includes spending policy actions:
import { useChipiSession } from "@chipi-stack/nextjs";

const USDC = "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb";

function SessionWithLimits() {
  const {
    session,
    hasActiveSession,
    createSession,
    registerSession,
    setSpendingPolicy,
    getSpendingPolicy,
    removeSpendingPolicy,
    isSettingSpendingPolicy,
    isRemovingSpendingPolicy,
  } = useChipiSession({
    wallet,
    encryptKey: pin,
    getBearerToken: getToken,
  });

  const setupWithLimits = async () => {
    // 1. Create and register session
    await createSession();
    await registerSession({ allowedEntrypoints: ["transfer"] });

    // 2. Set spending limits
    await setSpendingPolicy({
      token: USDC,
      maxPerCall: 1_000_000n,
      maxPerWindow: 50_000_000n,
      windowSeconds: 86400,
    });
  };

  const checkBudget = async () => {
    const policy = await getSpendingPolicy(USDC);
    const remaining = policy.maxPerWindow - policy.spentInWindow;
    console.log(`${remaining} USDC units remaining in this window`);
  };

  return (
    <div>
      {!hasActiveSession ? (
        <button onClick={setupWithLimits} disabled={isSettingSpendingPolicy}>
          Setup Session with $50/day Limit
        </button>
      ) : (
        <button onClick={checkBudget}>Check Remaining Budget</button>
      )}
    </div>
  );
}
The React hook automatically injects the current session’s sessionPublicKey. You only need to pass token, maxPerCall, maxPerWindow, and windowSeconds.

Python

from chipi_sdk import (
    ChipiSDK, ChipiSDKConfig,
    SetSpendingPolicyParams, SpendingPolicyConfig,
    GetSpendingPolicyParams, RemoveSpendingPolicyParams,
)

sdk = ChipiSDK(config=ChipiSDKConfig(
    api_public_key="pk_prod_...",
    api_secret_key="sk_prod_...",
))

USDC = "0x033068f6539f8e6e6b131e6b2b814e6c34a5224bc66947c47dab9dfee93b35fb"

# Set policy
tx_hash = sdk.sessions.set_spending_policy(
    SetSpendingPolicyParams(
        encrypt_key=user_encrypt_key,
        wallet=user_wallet,
        spending_policy_config=SpendingPolicyConfig(
            session_public_key=session.public_key,
            token=USDC,
            max_per_call=1_000_000,       # 1 USDC
            max_per_window=50_000_000,    # 50 USDC
            window_seconds=86400,         # 24 hours
        ),
    ),
    bearer_token=bearer_token,
)

# Query policy
policy = sdk.sessions.get_spending_policy(
    GetSpendingPolicyParams(
        wallet_address=user_wallet.public_key,
        session_public_key=session.public_key,
        token=USDC,
    )
)
print(f"Spent {policy.spent_in_window} of {policy.max_per_window}")

# Remove policy
sdk.sessions.remove_spending_policy(
    RemoveSpendingPolicyParams(
        encrypt_key=user_encrypt_key,
        wallet=user_wallet,
        session_public_key=session.public_key,
        token=USDC,
    ),
    bearer_token=bearer_token,
)
Async variants are available: aset_spending_policy, aget_spending_policy, aremove_spending_policy.

Validation Rules

The SDK validates before submitting to the contract:
RuleError
Token address must not be emptyINVALID_SPENDING_POLICY
windowSeconds must be a positive integer within u64 rangeINVALID_SPENDING_POLICY
maxPerCall must be non-negative and fit in u256INVALID_SPENDING_POLICY
maxPerWindow must be non-negative and fit in u256INVALID_SPENDING_POLICY
maxPerCall cannot exceed maxPerWindow (when maxPerWindow is set)INVALID_SPENDING_POLICY
Wallet must be CHIPI typeINVALID_WALLET_TYPE_FOR_SESSION

On-Chain Enforcement

The contract enforces spending limits during __execute__() for session-signed transactions:
  1. Tracked selectors: transfer, approve, increase_allowance, increaseAllowance
  2. Per-call check: If amount > maxPerCall, transaction reverts with “Spending: exceeds per-call”
  3. Window check: If spentInWindow + amount > maxPerWindow, transaction reverts with “Spending: exceeds window limit”
  4. Auto-reset: When now >= windowStart + windowSeconds, the window resets to zero
Owner-signed transactions (2-element signature) bypass spending policy enforcement entirely.