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:

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):

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

RuleEffect
MaxPerTransactionRuleDENY if amount_sats > max_sats
MaxPerDayRuleDENY if rolling 24h total would exceed cap
AddressWhitelistRuleDENY if recipient not in allowed set
AddressBlocklistRuleDENY if recipient is in blocked set
RequireApprovalAboveRuleREQUIRE_APPROVAL above threshold
FeeRateLimitRuleDENY if fee rate exceeds cap
DustProtectionRuleDENY if amount below 546 sats
CircuitBreakerRuleEmergency 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()]).