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 Case | Configuration |
|---|
| AI agent budgets | Max 0.01perAPIcall,50 per day |
| Game daily limits | Max 5 USDC per trade, 100 USDC per 24h |
| Employee wallets | Max 200 USDC per transaction, 1000 per week |
| DeFi automation | Max 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:
| Parameter | Type | Description |
|---|
maxPerCall | bigint (u256) | Maximum token amount per single transaction |
maxPerWindow | bigint (u256) | Maximum cumulative amount within the rolling window |
windowSeconds | number (u64) | Rolling window duration in seconds |
The contract also tracks:
| Field | Description |
|---|
spentInWindow | Amount spent in the current active window |
windowStart | Unix 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:
| Rule | Error |
|---|
| Token address must not be empty | INVALID_SPENDING_POLICY |
windowSeconds must be a positive integer within u64 range | INVALID_SPENDING_POLICY |
maxPerCall must be non-negative and fit in u256 | INVALID_SPENDING_POLICY |
maxPerWindow must be non-negative and fit in u256 | INVALID_SPENDING_POLICY |
maxPerCall cannot exceed maxPerWindow (when maxPerWindow is set) | INVALID_SPENDING_POLICY |
| Wallet must be CHIPI type | INVALID_WALLET_TYPE_FOR_SESSION |
On-Chain Enforcement
The contract enforces spending limits during __execute__() for session-signed transactions:
- Tracked selectors:
transfer, approve, increase_allowance, increaseAllowance
- Per-call check: If
amount > maxPerCall, transaction reverts with “Spending: exceeds per-call”
- Window check: If
spentInWindow + amount > maxPerWindow, transaction reverts with “Spending: exceeds window limit”
- Auto-reset: When
now >= windowStart + windowSeconds, the window resets to zero
Owner-signed transactions (2-element signature) bypass spending policy enforcement entirely.