Policy Engine
The Policy class enforces spend controls on every transaction proposal. Attach it to a wallet and every call to propose_payment is evaluated against the configured rules.
Basic Usage
from bitpilot import Policy
policy = Policy(
max_per_tx_sats=100_000,
max_per_day_sats=1_000_000,
require_approval_above_sats=50_000,
allowed_addresses=["tb1q..."],
max_fee_rate_sat_vb=500.0,
)
wallet.set_policy(policy)
Default Policy
If no arguments are given, Policy() uses conservative defaults:
- 100,000 sat per-transaction cap
- 1,000,000 sat daily rolling cap
- Approval required above 50,000 sats
- Fee rate cap at 500 sat/vB
Policy Decision Flow
propose_payment()
|
v
Policy.check(proposal)
|
+-- ALLOW ---------> proposal.status = "approved"
|
+-- REQUIRE_APPROVAL -> proposal.status = "pending"
|
+-- DENY -----------> proposal.status = "rejected"
PolicyCheckResult
Returned by policy.check(proposal):
decision—PolicyDecision.ALLOW,REQUIRE_APPROVAL, orDENYreason— human-readable explanationrule_name— which rule triggered the decisioncode— stable machine-readable string for agents (e.g.max_per_tx_exceeded,address_not_whitelisted); custom rules may omit itdetails— structured data (amounts, thresholds, limits)
Wallet propose_payment copies verdict fields onto TransactionProposal (policy_verdict, policy_code, policy_rule_name, policy_details) so listings and agent tools stay consistent.
Built-in Rules
| Rule | Effect |
|---|---|
MaxPerTransactionRule | DENY if amount_sats > max_sats |
MaxPerDayRule | DENY if rolling 24h total would exceed cap |
AddressWhitelistRule | DENY if recipient not in allowed set |
AddressBlocklistRule | DENY if recipient is in blocked set |
RequireApprovalAboveRule | REQUIRE_APPROVAL above threshold |
FeeRateLimitRule | DENY if fee rate exceeds cap |
DustProtectionRule | DENY if amount below 546 sats |
CircuitBreakerRule | Emergency kill-switch: DENY all when tripped |
CircuitBreakerRule
from bitpilot import CircuitBreakerRule
breaker = CircuitBreakerRule()
breaker.trip("suspicious activity detected")
# All proposals now denied
breaker.reset()
# Normal operation resumes
Daily Spend Tracking
policy.record_completed_spend(10_000)
print(policy.today_spent_sats())
print(policy.remaining_daily_budget_sats())
Custom Rules
Subclass Rule and implement evaluate(proposal) -> RuleResult:
from bitpilot.policy.rules import Rule, RuleResult, RuleDecision
class MyRule(Rule):
def evaluate(self, proposal):
if some_condition(proposal):
return RuleResult(decision=RuleDecision.DENY, rule_name="MyRule", reason="...")
return RuleResult(decision=RuleDecision.SKIP, rule_name="MyRule", reason="OK")
Pass custom rules via Policy(custom_rules=[MyRule()]).