Open standard · v0.2 draft · Apache-2.0

Agent Spend Policy Specification

An open, deterministic, fail-closed vocabulary for agent money-movement policy: the controls, reason codes, approval semantics, and tamper-evident receipt requirements that decide, before execution, whether an agent-initiated value transfer is allowed, held for a human, or denied. Published July 2026, derived from a production decision engine, and rendered here in full.

What this is

A shared language for the decision before money moves.

AI agents initiate payments: x402 pay-per-request calls, stablecoin transfers, refunds, payouts. This spec defines a rail-agnostic policy document format and evaluation semantics for deciding whether each transfer should be allowed, held for human approval, or denied. The decisions are deterministic and replayable, the failure mode is always closed (an error never silently allows), and every outcome carries stable, machine-readable reason codes.

It governs the decision, not the movement of funds. Transport, storage, key management, transaction signing, and rail-specific execution are out of scope, which is what lets one policy vocabulary span card rails, stablecoins, and pay-per-request protocols. It is complementary to protocol-level controls such as x402 payment extensions and wallet policy engines: those authorize keys and transactions; this spec expresses organization-level policy across rails.

It is also early, and we will say so plainly: this is our spec, published as a v0.2 draft for public comment. We are seeking readers, critics, and independent implementers, not claiming an installed base.

Implementations

Who implements it today.

The implementations today are ours, all Apache-2.0. The spec includes a conformance checklist (section 8 below) precisely so the next implementation does not have to be.

Reference implementation

@axiru/agent-spend-guardrails

The reference TypeScript evaluator: defineSpendPolicy and guardAgentSpend implement the full rule set, the precedence ladder, fail-closed error handling, and canonical fingerprints as a pure in-process library. Apache-2.0, zero dependencies, zero network calls.

On npm →

Types and registries

@axiru/spec

The vocabulary as code: OutboundValueTransfer types, policy schema v2, the rail registry, runtime type guards, and the reason-code registry. If you are building an independent implementation, this package is the typed surface of this document.

On npm →

Deployed gate

buzz-axiru

A gating MCP proxy for Block's Buzz agents that runs the reference evaluator in the decision path: payment-class tool calls are allowed, denied, or parked for human approval per this spec, and every decision lands in a hash-chained local log.

On GitHub →

Origin

Axiru hosted engine

The spec is derived from the production decision engine behind Axiru, which evaluates live agent-initiated transfers across card, stablecoin, and pay-per-request rails. The document is the extraction; the engine is where the semantics were earned.

Agent Controls →

The specification · Section 1

Purpose and scope.

This spec defines a rail-agnostic, deterministic policy format and evaluation semantics for deciding, before execution, whether an agent-initiated value transfer should be allowed, held for human approval, or denied. Requirement keywords MUST, MUST NOT, SHOULD, and MAY are used per RFC 2119.

In scope

The policy document format (JSON, schema_version: 2). Rule kinds and their matching semantics. Decision outcomes, precedence, and mode resolution (shadow vs enforcing). Canonical reason-code namespaces. Determinism and replay requirements. A conformance checklist.

Out of scope

Transport, storage, key management, transaction signing, and rail-specific execution. This spec governs the decision, not the movement of funds. Protocol and wallet layers authorize keys and transactions; this spec expresses organization-level policy across rails.

The specification · Section 2

Data model: the evaluation subject.

Policies are evaluated against a normalized outbound value transfer record with at least the following logical fields. Implementations MAY carry additional rail-specific context.

FieldTypeNotes
railstringRail identifier, e.g. "x402", "stripe", "usdc_solana".
rail_actionstringAction verb on that rail, e.g. "pay", "transfer", "refund".
amount.currencystringISO 4217 code or token symbol, uppercase canonical ("USD", "USDC").
amount.minor_unitsinteger (arbitrary precision)Smallest indivisible unit. Serialized as a base-10 integer string. Floats are forbidden.
initiator.kind"human" | "agent" | "automation"Who initiated the transfer.
initiator.idstringStable identifier for the initiator.
initiator.agent_metadata.modelstringPresent when kind is "agent".
initiator.agent_metadata.scopestringComma-separated scope tokens granted to the agent.
counterparty.kindstring"customer", "merchant", "wallet_address", "connect_account", or "unknown".
counterparty.idstringStable counterparty identifier.
counterparty.display.country_codestring (optional)ISO-3166-1 alpha-2, uppercase.
rail_event_attimestampWhen the transfer event occurred. Drives time-of-day rules.
policy_inputsobjectPrecomputed rolling-window aggregates; see below.
fingerprintstringsha256:<hex> over the canonical-JSON form; see section 7.2.

Rolling-window aggregates (policy_inputs)

Evaluation MUST be free of I/O (section 7.1), so velocity rules compare against aggregates computed by the caller and attached to the transfer record before evaluation.

FieldTypeMeaning
amount_24hinteger stringSum of minor units over the trailing 24 hours.
amount_30dinteger stringSum of minor units over the trailing 30 days.
count_24hintegerTransfer count over the trailing 24 hours.
count_30dintegerTransfer count over the trailing 30 days.

The scope of the aggregates (per organization, per agent, per counterparty) is a contract between the caller and the policy author. Implementations SHOULD document which scoping the caller supplies.

The specification · Section 3

Policy document format.

A policy is a JSON document. This example denies any transfer that would take an agent's trailing-24h spend to 100 USDC or beyond.

{
  "id": "policy_daily_cap_agent_1",
  "org_id": "local",
  "schema_version": 2,
  "name": "Daily cap for agent 1",
  "description": "Denies any transfer that would take trailing-24h spend to 100 USDC or beyond.",
  "version": 3,
  "author_id": "local",
  "mode": "enforcing",
  "rules": [
    { "kind": "initiator_id", "in": ["agent_1"] },
    { "kind": "rolling_window", "window": "24h", "aggregate": "sum_amount", "op": "gte", "value": "100000000" }
  ],
  "effect": {
    "kind": "deny",
    "reason_code": "guardrails.deny.daily_cap_exceeded",
    "reason_text": "Agent 1 exceeded its 24h spend cap"
  },
  "created_at": "2026-07-01T00:00:00Z",
  "updated_at": "2026-07-08T00:00:00Z"
}
FieldRequirement
schema_versionMUST be 2 for documents conforming to this spec.
idMUST be stable across edits to the same logical policy.
versionMUST increase monotonically on every edit. Persisted decisions reference the exact version evaluated (section 7.3).
modeOne of "shadow", "enforcing", "disabled". See section 6.3.
rulesArray of rule objects (section 4). Logical AND. An empty array matches every transfer.
effectExactly one effect object (section 5).
org_id, author_idTenancy and authorship metadata. Standalone implementations MAY use "local".
The specification · Section 4

The ten rule kinds.

A policy matches a transfer if and only if every rule in rules matches. Empty membership lists ("in": []) do not constrain and MUST match, consistent with "a rule that names nothing narrows nothing".

4.1 rail

{ "kind": "rail", "in": ["x402", "usdc_solana"] }

Membership on the transfer's rail identifier. An empty in list matches; a rail in the list matches; otherwise no match.

4.2 rail_action

{ "kind": "rail_action", "in": ["refund", "payout"] }

Same membership semantics as rail. Action strings vary by rail; the evaluator MUST NOT enforce cross-rail action-name compatibility. That is the policy author's responsibility.

4.3 amount

{ "kind": "amount", "currency": "USDC", "gte": "50000000", "lte": "1000000000" }

currency denominates gte and lte, which are integer strings compared with arbitrary-precision arithmetic. An optional fx_base converts the transfer amount first via a caller-injected pure FX resolver and MUST equal currency. A currency mismatch with no fx_base is a rule evaluation error (section 6.4), never a silent match or non-match by value.

4.4 initiator_kind

{ "kind": "initiator_kind", "in": ["agent", "automation"] }

Membership on initiator.kind (human, agent, or automation).

4.5 initiator_id

{ "kind": "initiator_id", "in": ["agent_procurement_1"] }

Membership on initiator.id.

4.6 agent_scope

{ "kind": "agent_scope", "has_all": ["payments.create"], "has_none": ["payments.unlimited"] }

Both lists empty or absent: match. Non-agent initiator with a non-empty has_all: no match; non-agent with only has_none set: match. An agent lacking any has_all scope, or holding any has_none scope: no match. An agent record missing scope metadata is a rule evaluation error (fail closed).

4.7 counterparty

{ "kind": "counterparty", "kind_in": ["merchant"], "id_in": ["m_1"], "country_in": ["US"], "country_not_in": ["KP"] }

All sub-fields optional and independently ANDed. country_in with no country on the transfer is a no-match; country_not_in with no country on the transfer matches (absence cannot be in the list). Country codes are uppercase ISO-3166-1 alpha-2.

4.8 rolling_window

{ "kind": "rolling_window", "window": "24h", "aggregate": "sum_amount", "op": "gte", "value": "100000000" }

window is 24h or 30d; aggregate is sum_amount (minor units) or count; op is gte or lte; value is an integer string of arbitrary precision. An optional group_by is a grouping hint: implementations that do not compute per-group aggregates MUST treat a set group_by as a rule evaluation error (fail closed), never as an org-level comparison. The caller-supplied aggregates in policy_inputs describe PRIOR activity and exclude the transfer under evaluation. For sum_amount the comparison is request-inclusive: implementations MUST add the transfer's amount.minor_units to the prior aggregate before applying op, so a single transfer cannot leap over an amount cap that its prior spend alone would not have hit. For count the comparison is against the prior count only (every transfer contributes exactly 1, so no equivalent jump exists). See also the zero-sentinel requirement in 6.5.

4.9 time_of_day

{ "kind": "time_of_day", "tz": "America/New_York", "ranges": [{ "start": 9, "end": 17 }] }

Matches when rail_event_at, projected into the IANA timezone tz, falls in ANY range. Ranges are whole hours, inclusive start, exclusive end. Cross-midnight ranges (start > end) mean [start, 24) union [0, end). An invalid timezone or an empty ranges array is a rule evaluation error. DST MUST be honored via the timezone database.

4.10 custom_expression

{ "kind": "custom_expression", "expression": "!(ovt.counterparty.id in [\"m_1\", \"m_2\"])" }

A sandboxed boolean expression over the transfer record. Conforming evaluators MUST guarantee determinism (no clock, randomness, or I/O; the only inputs are the transfer fields and the injected evaluation clock now, in epoch milliseconds), isolation (no access to host globals, prototypes, or processes), a bounded evaluation budget (exhaustion is a rule evaluation error), and at minimum this grammar: &&, ||, !, comparisons, in [literals], parentheses, string, number, boolean, and null literals, and dotted field paths rooted at ovt. Expressions that do not evaluate to a boolean are rule evaluation errors.

The specification · Section 5

Effects, decision outcomes, and reason codes.

Every evaluation produces exactly one outcome: allow (execute the transfer), require_approval (hold; a human decides; also the mandatory fail-safe for degraded evaluations), or deny (do not execute). A decision MUST carry a non-empty, ordered reason list: the first reason is the winning reason, and subsequent entries record other matched policies for audit transparency.

The effect object

{
  "kind": "deny",
  "reason_code": "customer.deny.embargo",
  "reason_text": "Embargoed country",
  "approver_group": "finance",
  "max_authorized_amount": { "currency": "USDC", "minor_units": "50000000" },
  "authorization_ttl_seconds": 60
}
FieldRequirement
kind"allow", "deny", or "require_approval".
reason_codeRequired. Stable, namespaced, machine-readable.
reason_textRequired. Human-readable. Consumers MUST NOT parse it.
approver_groupOptional routing hint for require_approval.
max_authorized_amountOptional clamp propagated to downstream authorization tokens.
authorization_ttl_secondsOptional token TTL; implementations SHOULD cap it (the reference engine caps at 300s).

Reason-code namespaces

Reason codes are dot-separated, lowercase, stable identifiers of the form namespace.family.detail, where family is allow, deny, or pending. Canonical engine codes live under the guardrails.* namespace:

CodeEmitted when
guardrails.allow.defaultNo policy matched; default allow.
guardrails.deny.amount_exceededEngine summary for a policy-driven deny.
guardrails.deny.scope_violationAgent scope constraint denial.
guardrails.deny.rolling_window_exceededVelocity limit denial.
guardrails.deny.counterparty_blockedCounterparty policy denial.
guardrails.deny.country_blockedCountry policy denial.
guardrails.deny.unknown_railTransfer arrived on a rail the evaluator has no support for; failed closed.
guardrails.deny.shadow_mode_forcedSummary marker: the highest-precedence match was shadow-mode; outcome not enforced.
guardrails.deny.expression_timeoutA rule could not be evaluated (parse failure, budget exhaustion, FX miss, currency mismatch); category code, not a specific defect.
guardrails.pending.approval_requiredEngine summary for a policy-driven require_approval.
guardrails.pending.high_valueHigh-value escalation.
guardrails.pending.velocity_inputs_unavailableVelocity rules in scope but aggregates unavailable (section 6.5).

Prefix rules: hosted or embedded implementations MAY emit these same codes under their own vendor prefix (for example, a hosted platform emitting axiru.pending.approval_required for guardrails.pending.approval_required); the family.detail suffix MUST be preserved so codes remain translatable, and public tooling SHOULD normalize vendor prefixes to guardrails.*. User-defined policies MUST use their own namespace (the convention is customer.*) and MUST NOT use guardrails.* or any vendor's reserved prefix. Implementations MUST reject user policies whose reason codes squat on reserved prefixes.

The specification · Section 6

Evaluation semantics.

AND within a policy, OR across policies: a policy matches iff every rule matches (an empty rule list matches everything), every non-disabled policy is checked against every transfer, and all matches feed the precedence ladder. Authors express OR conditions with multiple policies.

6.2 Precedence

deny  >  require_approval  >  allow

Among matched, enforcing policies, one matched deny wins outright, regardless of how many allow or require_approval policies also matched. Within the same effect tier, implementations MUST apply a deterministic tiebreak; the reference implementation uses policy version descending, then policy id ascending, so replays reproduce the identical winner. If no policy matches, the decision is allow with guardrails.allow.default. (Deployments wanting default-deny express it as an explicit catch-all policy with an empty rule list.)

6.3 Shadow vs enforcing

Enforcing policies are evaluated, affect the outcome, and are recorded in reasons. Shadow policies are evaluated and recorded but MUST NOT change the outcome. Disabled policies are not evaluated at all. When only shadow policies matched, the outcome is allow and the decision MUST carry a distinguishable marker (guardrails.deny.shadow_mode_forced as the summary) so operators can see what enforcement would have done. Every new policy SHOULD begin life in shadow mode; this is the graduated-autonomy on-ramp.

6.4 Fail closed on rule evaluation errors

A rule that cannot be evaluated (unparseable expression, invalid timezone, currency mismatch without FX, budget exhaustion, unknown rule kind from a newer spec revision) makes its policy non-matching AND degrades the decision: a clean allow MUST be demoted to require_approval with a diagnostic reason. An evaluation error MUST NOT silently produce allow. Errors on shadow-mode policies MUST NOT degrade live traffic. At the transfer level, a rail the implementation cannot evaluate MUST produce deny with guardrails.deny.unknown_rail.

6.5 Zero-sentinel velocity escalation

If at least one enforcing policy in scope contains a rolling_window rule and the transfer's policy_inputs are all zero (the sentinel emitted by callers that did not compute aggregates), the evaluator cannot distinguish no prior activity from aggregates missing. A gte-deny rule would be unfireable and an lte-allow rule trivially true, so a clean allow MUST be demoted to require_approval with guardrails.pending.velocity_inputs_unavailable. Existing require_approval and deny outcomes keep their action and gain the diagnostic reason. Known accepted false positive: an org or agent with genuinely zero history receives one conservative approval on its first transfer under a velocity policy.

6.6 Decision record: a conforming decision record contains at least the outcome, the ordered reason list (each with reason_code, reason_text, and the matching policy's id, version, and mode when applicable), the list of matched policy ids in precedence order, a single summary reason code, the transfer fingerprint, the evaluation clock used, and a deterministic decision id derived from the fingerprint.

The specification · Section 7

Determinism and replay.

Evaluation MUST be a pure function of the transfer record, the policy set, and the evaluation clock. No I/O during evaluation: anything the evaluator needs, including rolling-window aggregates and FX rates, is resolved beforehand and passed in. No ambient clock reads; now is an explicit input. No randomness; decision ids are derived deterministically. Injected functions such as FX resolvers MUST themselves be pure and deterministic for a given (from, to, at) triple.

7.2 Canonical fingerprint

Each transfer record MUST carry a fingerprint of the form sha256: followed by 64 lowercase hex characters, computed over a canonical JSON serialization of at least (rail, rail_action, amount, initiator, counterparty, context). Canonical means object keys sorted lexicographically at every nesting level, amounts serialized as base-10 integer strings (floats, NaN, and Infinity rejected), and no insignificant whitespace. Two structurally identical transfers MUST produce identical fingerprints regardless of construction order. The fingerprint is the replay and idempotency key.

7.3 Replay

Given the persisted transfer record, the policy documents at their evaluated versions, and the evaluation clock, re-running evaluation MUST reproduce the original decision bit for bit: same outcome, same reason codes in the same order, same decision id. This is why policy version is monotonic and persisted per decision, and why timestamps and aggregates travel with the transfer record rather than being re-derived at replay time.

The specification · Section 8

Conformance checklist.

An implementation may claim conformance with Agent Spend Policy Spec v0.2 if all of the following hold. If you are building one, this list is the finish line, and the reference test suite is Apache-2.0 alongside the reference implementation.

Document format

  • Accepts policy documents with schema_version: 2 and all fields in section 3.
  • Rejects user policies whose effect.reason_code uses guardrails.* or a vendor-reserved prefix.

Rules

  • Implements all ten rule kinds of section 4 with the stated match semantics, or treats unimplemented kinds as rule evaluation errors (never as matches or silent skips).
  • Empty membership lists match; empty time_of_day.ranges errors.
  • Amount and rolling-window comparisons use arbitrary-precision integers; no floating point in any monetary path.
  • custom_expression evaluation is sandboxed, budgeted, and deterministic per 4.10.

Evaluation

  • AND within a policy, OR across policies (6.1).
  • Precedence deny > require_approval > allow with a deterministic tiebreak (6.2).
  • Shadow policies recorded but never enforced; shadow-only matches carry the marker summary (6.3).
  • Rule evaluation errors demote clean allows to require_approval and never silently allow (6.4).
  • Unknown rails produce deny with guardrails.deny.unknown_rail (6.4).
  • Zero-sentinel aggregates with enforcing velocity policies demote clean allows to require_approval (6.5).
  • Decisions carry a non-empty ordered reason list, winner first (5.2, 6.6).

Determinism

  • Evaluation is pure: no I/O, no ambient clock, no randomness (7.1).
  • Canonical sha256: fingerprints per 7.2.
  • Bit-for-bit replay per 7.3.
The specification · Section 9

Versioning and change policy.

This is a v0.2 draft. Spec changes are versioned. Additive changes (new rule kinds, new reason codes) bump the minor version; evaluators MUST treat unknown rule kinds from newer minors as rule evaluation errors (fail closed), which keeps older evaluators safe against newer policies. Any change to matching semantics, precedence, or the fingerprint algorithm is a breaking change: it bumps schema_version and is named explicitly in the changelog.

The canonical text lives in the open source repository and this page tracks it. If the two ever disagree, the repository file wins, and we would treat the disagreement as a bug here.

The canonical spec file on GitHub →

Contribute

Feedback and independent implementations are welcome.

The most useful contributions right now: readings of the semantics that find ambiguity, implementations in other languages that exercise the conformance checklist, and rule kinds you needed that the vocabulary lacks. All of it goes through GitHub issues on the open source repository.

Open an issue → · github.com/AxiruAI/axiru-oss → · The open source packages →

Next step

The spec is the language. The packages speak it.

The reference evaluator runs in your process with no account and no network calls, and the hosted product runs the same semantics inside a live enforcement path with an approval queue and a hash-chained ledger.

Start in shadow mode first. Move to live enforcement later.

We use cookies for analytics and marketing measurement. You can reject non-essential cookies at any time.

Privacy policy
Agent Spend Policy Specification v0.2 | Open Standard | Axiru | Axiru