@axiru/agent-spend-guardrails
v0.1.1
Published
Deterministic spend guardrails for AI agents. Define policies, evaluate spend intents, get allow / require_approval / deny with stable, replayable reason codes. No SaaS dependency.
Maintainers
Readme
@axiru/agent-spend-guardrails
Spend guardrails for AI agents in 30 lines of TypeScript.
Your agents can already move money: x402 micropayments, stablecoin transfers, Stripe refunds. This package answers the question every one of those transfers should pass through first: is this agent allowed to make this payment, right now, to this counterparty, at this amount?
allow | require_approval | denyDeterministic. Replayable. No SaaS dependency. Apache-2.0.
Why this exists
Protocol-level controls (x402 payment extensions, wallet policy engines, per-key spending limits) answer "can this key sign this transaction". They are necessary and this package is complementary to them, not a replacement. What they cannot answer is the org-level question: is this spend consistent with your rules across every rail your agents touch, with an audit trail a human can replay?
agent-spend-guardrails is that org-level layer, extracted from the production decision engine behind Axiru. It runs entirely in your process:
- Deterministic. Same intent, same policies, same history, same timestamp: identical decision, identical reason codes, identical
sha256:fingerprint. No wall clock reads (when you pass a timestamp), no I/O, no randomness. - Replayable. Every result carries a canonical-JSON fingerprint and a decision id derived from it. Persist the inputs and you can reproduce any decision bit for bit, years later.
- Fail closed. Unknown rails, unevaluable rules, and missing velocity aggregates never fall through to a silent allow. The worst case is always
require_approvalordeny. - Zero heavy dependencies. Node 18+,
node:crypto, and the pure evaluator. Nothing phones home.
Quickstart
npm install @axiru/agent-spend-guardrailsimport {
defineSpendPolicy,
guardAgentSpend,
humanApprovalAboveAmount,
perAgentDailyCap
} from "@axiru/agent-spend-guardrails";
const policies = [
// Anything at or above 50 USDC goes to a human first.
humanApprovalAboveAmount({ currency: "USDC", threshold_minor_units: "50000000" }),
// This agent may spend at most 100 USDC per rolling 24h.
perAgentDailyCap({ agent_id: "agent_procurement_1", currency: "USDC", cap_minor_units: "100000000" }),
// And a custom rule: never let agents pay wallets in embargoed countries.
defineSpendPolicy({
name: "Block embargoed countries",
rules: [{ kind: "counterparty", country_in: ["KP", "IR"] }],
effect: { kind: "deny", reason_code: "customer.deny.embargo", reason_text: "Embargoed country" }
})
];
const result = guardAgentSpend({
intent: {
rail: "x402",
action: "pay",
amount: { currency: "USDC", minor_units: "12000000" }, // 12 USDC, always integer strings
agent: { id: "agent_procurement_1", model: "claude-sonnet-4-6", scope: "payments.create" },
counterparty: { id: "https://api.datavendor.example/reports", kind: "merchant" },
timestamp: new Date()
},
policies,
history: { amount_24h: "38000000", count_24h: 6 } // this agent's prior 24h spend
});
// result.decision -> "allow" | "require_approval" | "deny"
// result.reason_code -> e.g. "guardrails.deny.daily_cap_exceeded"
// result.reasons -> full audit trail, winner first
// result.fingerprint -> "sha256:..." replay and idempotency keyExecute the transfer only when result.decision === "allow". On require_approval, hold it and route to a human. On deny, drop it and log the reasons.
Use it from your own agent code
Most adopters wire this into a custom agent built on an SDK or framework. The examples/ directory has three worked integrations with a shared README: a LangGraph payment tool node, a CrewAI tool that calls the hosted decision API from Python, and a plain Anthropic SDK tool-use loop with a guarded executor, plus a runnable demo (pnpm example after a build). The core is always the same ten lines, shown here exactly as they appear in this package's test suite (testReadmeTenLineExample, a passing test):
const policy = defineSpendPolicy({
name: "Deny large transfers",
rules: [{ kind: "amount", currency: "USDC", gte: "10000000" }],
effect: { kind: "deny", reason_code: "customer.deny.too_large", reason_text: "Over the limit" }
});
const result = guardAgentSpend({
intent: {
rail: "usdc_solana", action: "transfer",
amount: { currency: "USDC", minor_units: "25000000" },
agent: { id: "agent_1" }, counterparty: { id: "vendor_api" },
timestamp: new Date("2026-07-08T12:00:00.000Z")
},
policies: [policy]
});Here result.decision is "deny" and result.reason_code is "customer.deny.too_large": 25 USDC against a 10 USDC ceiling. Put those lines in front of your tool executor and no payment tool can fire without a decision.
The API
Two functions. That is the whole surface.
defineSpendPolicy(init)
Builds a policy document conforming to the Agent Spend Policy Spec v0.2 (schema_version: 2), with sensible defaults: enforcing mode, version: 1, local org. Rules within a policy are ANDed; separate policies are ORed. Ten rule kinds are available: rail, rail_action, amount, initiator_kind, initiator_id, agent_scope, counterparty, rolling_window, time_of_day, and custom_expression (a sandboxed, budgeted, deterministic expression language).
guardAgentSpend({ intent, policies, history })
Evaluates one spend intent against the policy set and returns { decision, reason_code, reasons, summary_code, fingerprint, decision_id, evaluated_at }.
intentis the simplified shape shown above: rail, action, amount (currency + integer-string minor units), agent, counterparty, timestamp.historyis optional precomputed rolling-window aggregates (amount_24h,amount_30d,count_24h,count_30d) covering PRIOR activity only. The evaluator never does I/O, so velocity rules compare against whatever you supply. Scope the aggregates to match your policy's intent: per-agent caps want per-agent sums.sum_amountcomparisons are request-inclusive (the engine adds the intent under evaluation before comparing), so a single oversized transfer cannot leap an amount cap.- Precedence:
denybeatsrequire_approvalbeatsallow. One matched deny wins no matter how many allows also matched.
Zero-sentinel escalation: if an enforcing policy in scope has a rolling_window rule and you supply no history (or all zeros), the guard cannot tell "no prior activity" from "forgot to compute aggregates". It demotes a clean allow to require_approval with guardrails.pending.velocity_inputs_unavailable. A brand-new agent's first transfer under a velocity policy gets exactly one conservative approval. This is deliberate and inherited from the production engine.
Presets
| Preset | What it does | Effect | Reason code |
| --- | --- | --- | --- |
| perAgentDailyCap | Trailing-24h spend cap for one agent | deny | guardrails.deny.daily_cap_exceeded |
| humanApprovalAboveAmount | Route single transfers at or above a threshold to a human | require_approval | guardrails.pending.above_approval_threshold |
| counterpartyAllowlist | Deny payment to any counterparty not on the list | deny | guardrails.deny.counterparty_not_allowlisted |
| businessHoursOnly | Block (or escalate) spend outside business hours in an IANA timezone | deny or require_approval | guardrails.deny.outside_business_hours |
| velocityCountCap | Circuit breaker on transfer count per window (catches runaway retry loops) | require_approval or deny | guardrails.pending.velocity_count_exceeded |
Every preset accepts mode: "shadow" to observe before enforcing.
Graduated autonomy
The intended adoption path, and the one the hosted platform is built around:
- Shadow. Ship every policy with
mode: "shadow". Decisions stayallow, but the reason trail records what would have happened (guardrails.deny.shadow_mode_forcedinsummary_code). Watch it for a billing cycle. - Enforce with a human lane. Flip to
enforcingwithrequire_approvaleffects. Agents keep working; the risky tail waits for a person. - Widen autonomy. As an agent earns trust, raise its caps and convert approval lanes to allows. Tighten instantly by editing a policy; no agent redeploys.
Determinism and replay
Every decision is a pure function of (intent, policies, history, timestamp). The result's fingerprint is a SHA-256 over the canonical-JSON form of the intent (sorted keys at every level, integer-string amounts, no floats), computed with node:crypto. Store the inputs alongside fingerprint and decision_id and you have an audit log you can replay against any future version of the engine to detect drift.
If you want that as a service (a tamper-evident evidence ledger, approvals inbox, multi-rail ingestion, decision replay across policy versions, SOC 2 export), that is Axiru's hosted platform, which runs this exact evaluator. The OSS package is complete without it.
Relationship to protocol-level controls
| Layer | Example | Question answered | | --- | --- | --- | | Key / wallet | Per-key spending limits, MPC policy engines | Can this key sign this transaction? | | Protocol | x402 payment extensions, facilitator limits | Is this payment well-formed for this rail? | | Org (this package) | agent-spend-guardrails | Is this spend consistent with our rules, across all rails, with a replayable audit trail? |
Run all three. Protocol controls cannot see cross-rail velocity or org-wide counterparty policy; org controls cannot stop a leaked key. They compose.
Spec
The policy document format, rule semantics, precedence ladder, and reason-code namespaces are specified in the Agent Spend Policy Spec v0.2 (draft), published July 2026 under Apache-2.0 and derived from the production engine. Conforming implementations exist in TypeScript (this package and the hosted engine); the spec includes a conformance checklist for independent implementations.
Related packages
@axiru/specis the shared policy and evidence vocabulary this package speaks.@axiru/x402-policy-middlewareapplies the same decision to x402 facilitator flows.@axiru/x402-receipt-verifierverifies the evidence that comes back after settlement.
License
Apache-2.0. Copyright 2026 Axiru. See LICENSE.
