Beersy
BRC-181

Wallet-Enforced Autonomous-Agent Spend Policy

An autonomous piece of software, like an AI agent making tiny automatic payments, can't stop and ask a human to approve every single spend, but if you just hand it a wallet key or a monthly budget, a single hijacked request can drain the whole thing to an attacker in one shot. Before this, the only tools were a per-app monthly ceiling or a one-time human approval prompt, neither of which limits who the money goes to, how fast it moves, or what happens when spending patterns look wrong.

RexStarBSVchanged 25 Sept 202666 min read

Reference for an AI

Everything an assistant needs to answer questions about BRC-181 accurately, including what it depends on.

Summary

Why
An autonomous agent that must spend money without a human confirming every transaction can be prompt-injected into trying to send all its funds to an attacker, and a simple monthly spending ceiling does nothing to stop that.
What
BRC-181 is a wallet-enforced spend policy that binds an autonomous agent to a signed, capped, destination-restricted allowance so it can spend unattended without ever exceeding an operator-authorized envelope.
How
The operator signs a PolicyRecord (per-tx, period, lifetime caps, a canonical destination allowlist, rate and circuit-breaker limits, an expiry) into an isolated agent account, the agent presents a bearer token on each , and the wallet's enforcement engine checks that request against the policy under an…

What this lets you do

  • Let an agent spend from a dedicated capped account with no per-transaction prompt
  • Bind per-transaction, rolling-period, and lifetime spend caps to a signed policy
  • Restrict spends to a canonical destination allowlist that closes the raw-script bypass
  • Reject or escalate requests atomically so concurrent spends can't overspend or double-select coins
  • Revoke an agent's spending authority instantly by invalidating its signed policy

Written by claude-sonnet-5 from the specification text. Where the two differ, the original is correct.

agent requestsigned policyallow/escalate

The specification

Abstract

This BRC specifies a wallet-enforced spend policy that lets an autonomous software agent (e.g. an LLM-driven MCP client) spend from a dedicated, isolated wallet account without a human in the loop for every transaction, while guaranteeing that the agent can never move funds outside a signed, operator-authorized envelope. It defines: (1) a signed, canonicalized PolicyRecord binding an agent identity to per-transaction / rolling-period / lifetime caps, a canonical destination allowlist, rate and circuit-breaker limits, and an expiry; (2) a canonical destination format over both hash160 and arbitrary script-hash, closing the raw-script allowlist bypass; (3) the exact, ordered set of checks a wallet MUST perform per createAction, with an atomic check→reserve→debit ledger that is safe under concurrent (high-frequency) spends; (4) fail-closed rules for broadcast uncertainty, freezes, and malformed requests; and (5) instant revocation. The policy is enforced entirely by the wallet; nothing in the agent's request is trusted except as input to be validated against the policy. A second implementer can build an interoperable enforcement engine from the Specification and Test Vectors alone.

Motivation

Machine-to-machine nanopayments are a defining use case for a low-fee BSV wallet: agents paying per-query metered endpoints that settle to a stable address, bidding within a budget on a marketplace, anchoring inscriptions, and paying other agents. One prominent 402 profile is not in that list, and the boundary is stated under Relationship to related work rather than left for an implementer to discover. The wallet-to-application for this already exists, BRC-100 defines createAction/signAction and the per-application permission model, and BRC-1 defines the underlying transaction-creation primitive. What is missing is a way for the wallet to authorize headless spends safely.

The closest existing authorization mechanism, BRC-116 (Wallet Permissions and Trust), lets an application declare a spendingAuthorization in its : an {amount, description} pair documented as a "Monthly spending limit in ", which the wallet grants through a permission prompt, optionally bundled with other declared permissions into a single group prompt. It is a ceiling plus a grant, not an enforcement envelope: within the granted amount it constrains how much, not to whom, how fast, or under what conditions, and it carries no rate limit, no destination allowlist, no circuit breaker, and no signed record a wallet can verify independently of the session that granted it.

  1. A per-transaction prompt defeats autonomy. An agent paying 1 sat per query thousands of times per minute cannot stop for a human confirmation on each spend. A monthly ceiling alone, without a per-tx cap, destination allowlist, rate limit, and circuit breaker, is too blunt to be safe for headless operation, a single prompt-injected request could drain the whole monthly budget to one attacker output.

  2. An LLM-driven agent is an untrusted requester. An agent will eventually be prompt-injected into trying to send everything to an attacker's address. The security boundary therefore cannot be the agent's good behaviour; it must be a wallet-side policy engine that treats every field of the agent's request as adversarial input to validate, never as an instruction to trust.

  3. Autonomy needs isolation and fine-grained bounds. Autonomous authority must be scoped to a dedicated, capped account (blast radius = the account balance, never the main wallet) and bounded on several independent axes at once (per-tx, period, lifetime, destination, rate, anomaly), because any single axis is individually bypassable.

The risk this addresses is not hypothetical, and it is not only the author's view. BRC-151 (BRC-100 Risk Assessment and Best Integration Practices), an opinion paper merged into this repository, devotes its Section 9 to AI agents operating with human funds and reaches the same two conclusions independently: that "small spending limits do not eliminate the danger" because an agent "may make repeated micropayments, accumulate losses" within any single-axis ceiling, and that the danger "becomes especially severe when the AI agent's operating wallet is also the human user's primary" wallet. It observes that a human "may believe that a spending authorization defines the purpose for which money can be used" when it does not.

BRC-151 states the problem and declines to specify a remedy, which is proper for an opinion. This standard is one remedy: multi-axis limits so a ceiling cannot be drained by repetition, and a required isolated account so the agent's blast radius is never the user's primary balance. This BRC complements BRC-116 rather than replacing it. BRC-116 answers may this application spend, and up to how much this month; this standard answers may this specific action proceed right now, unattended, given everything this agent has already spent. The two compose: a wallet MAY require a BRC-116 grant before an agent policy is honoured at all.

This standard sits alongside the Wallet Permission Prompt Liveness Contract (merged as BRC-219). The two cover different cases. That standard governs the attended case: an application sends a request that needs user consent, and the wallet may keep the request pending until the person grants or denies it, however long that takes. This standard governs the case where there is no person to ask. The wallet holds signed, bounded limits and checks them on every action, so an autonomous agent can spend inside the envelope with no prompt per transaction.

The same division applies to BRC-116. Its spendingAuthorization bounds a total over a period; it does not describe per-action enforcement, and its policy-gating and whitelist provisions (§ Policy gating, Whitelist handling) already contemplate a wallet allowing an operation without prompting. What is absent there, and specified here, is the signed, wallet-verified envelope that makes such an allowance safe to grant to software that no human is watching.

Bounded agent spending without a wallet: BRC-172

BRC-172 (Giving an AI Agent Control of a Delivery Vehicle Without Giving It a Wallet) addresses the same danger from the opposite premise. It defines a covenant that carries its own fuel and enforces its own successor state, so the agent holds at most a credential that advances the covenant and, in that standard's phrase, has "control without custody". Where there is no key, there is nothing for a prompt-injected request to redirect.

The two standards apply to different topologies of value, and neither subsumes the other.

BRC-172This standard
Where the value sitspre-committed into a covenantin a wallet account the agent may spend from
What the agent holdsa credential that advances a scriptan authenticated request to a wallet
Destinationsfixed by the script when the covenant is createdany destination on a signed allowlist, revisable without moving funds
Bound enforced bythe , at consensusthe wallet, per createAction, against a stateful ledger

A covenant battery is the stronger construction wherever the payment graph can be committed in advance. It does not reach the case this standard is written for: an agent paying varying external counterparties whose identities are not known when authority is granted, such as metered per-query endpoints, marketplace bids, or payments to other agents discovered at run time. Committing that graph into a covenant ahead of time is not possible, so the bound has to be applied at the moment of spending by the party that holds the key.

BRC-172 is explicit about its own boundary. Its section states: "Does not define what the covenant computes, or how its state is encoded", and it warns that "A battery does not make an agent correct, honest or safe to deploy." This standard makes no broader claim: it bounds what an untrusted requester can cause a wallet to do, and it does not make the agent trustworthy either.

An implementer serving both cases can use both. A covenant battery for the committed portion of an agent's work, and a wallet-enforced policy for the portion that must remain open.

The method an agent may reach, and the amount it may move: BRC-179

BRC-179 (Signed Capability Manifest for Wallet and Tool-Server Method Exposure) bounds the same agent from the other side of the call. It defines a risk-tiered manifest of exactly which methods a wallet or tool-server exposes, in which every tier that moves value is withheld until an operator affirmatively allows it, and a fingerprint of the declared set that is pinned into the build so an edited manifest cannot widen the surface at run time. Its Section 11 places that gate before any work is done, "before BRC-100 permissioning, before BRC-103 authentication", so a method that is not declared and enabled is never reached by a caller at all.

That gate and this policy are sequential, not alternative, and each is blind to what the other decides.

BRC-179This standard
Question answeredis createAction exposed on this build at allmay this action proceed, now, unattended
Unit of decisionone method, a booleanone request, against accumulated state
Bound tothe buildone (agent_account, agent_identity) pair
Revised bya rebuild, which moves a published fingerprintan operator signature, which moves an envelope
Evaluatedonce, before any caller is consultedevery request, after the caller is authenticated
Cannot seeamounts, destinations, rates, spend historywhether the method should have been exposed

BRC-179 is a precondition of this standard rather than a component of it. Because its spend-bearing tier is default-off, an operator who wants an agent to spend must affirmatively expose the method; what this standard supplies is the reason that exposure can be safe to grant. A manifest cannot express an amount, a destination or a rate, because a capability decision is caller-independent by construction, and BRC-179 Section 14 draws the same line from its own side: the manifest "does not replace user consent, it bounds the menu consent can be asked about". A policy record, conversely, cannot prevent a method from existing on a surface it never sees. Neither reduces to the other, and their failure modes are independent: widening a manifest moves a fingerprint that is published off-box, while widening a policy requires an operator signature that is carried in the envelope.

An implementation deploying both MUST evaluate them in that order and MUST NOT let either stand in for the other. If the manifest denies, this standard never runs and no policy check is reached. If the manifest allows, every spend through the exposed method remains subject to Section 5 in full. A wallet MUST NOT treat an enabled spend capability as evidence that any policy permits a spend, and MUST NOT treat a valid policy as grounds to expose a method the manifest withholds.

A payee whose address changes per invoice: BRC-166

BRC-166 (P2PKH Payments for HTTP 402) specifies a publicly-verifiable profile for HTTP 402 Payment Required on BSV, and its Section 5.4 is categorical: the "MUST issue a fresh receiving address for every invoice, and MUST NOT reuse an address across invoices". The derivation is "an origin-local matter and is deliberately not specified", and the payer "neither knows nor needs to know" it.

This standard cannot bound such a payee, and an implementer should know that before designing around it. Section 3.1's dest_allowlist is a signed enumeration of canonical destinations. A BRC-166 origin presents a destination that is fresh per invoice and that the payer cannot derive, predict or verify, so there is nothing to enumerate at signing time. Every invoice would fail check 5 as off-allowlist, and on a policy that escalates rather than rejects, every invoice would refer to a human, which is the opposite of the unattended operation this standard exists to provide. Nor does the narrow admission path of Section 5.4 reach it: that admits an address the wallet has verified as controlled by an identity already in dest_allowlist, and a BRC-166 address is derived from a secret the origin does not share.

The gap is real rather than editorial, and closing it needs a bound this version does not define: a signed entry naming the payee rather than the destination, so that an address arriving inside an invoice from that payee is admitted under a per-invoice cap, a period cap and a rate limit. That moves the boundary from which address to which counterparty, how much, how fast, and it widens what a signature authorizes, so it belongs in a revision with its own vectors and threat analysis rather than in a late addition to this one. Until then, a wallet MUST NOT admit a per-invoice destination by any looser reading of Section 5.4, and an operator whose agent must pay a BRC-166 origin should treat that as outside this standard's scope.

Why this is not a reserved-namespace module

BRC-98 (P Protocols) and BRC-111 (P Labels) reserve the p prefix so future schemes can extend wallet behaviour without colliding, in protocol identifiers and in action respectively. BRC-177 uses the second of these, defining the BRC-111 nosend and labels of the form p nosend expiry seconds <duration>. That is a working precedent for module-defined wallet behaviour, and it is worth stating plainly why this standard does not follow it.

A BRC-111 label is carried by one action and describes that action. BRC-111 itself frames labels as organizational metadata whose permissioning a module may extend. The subject of this standard is not an action. It is a signed record with a lifecycle of its own, granted, versioned, superseded and revoked independently of any transaction, and evaluated against accumulated state that spans every action an agent has taken inside a period. Expressing it as a label would attach a standing authorization to one of the many events it governs, and would leave the record itself with no defined home.

The permission lifecycle that this standard needs is the one BRC-116 already specifies: declaration, grant, renewal, revocation and persistence. That is why this standard extends BRC-116 rather than claiming a reserved namespace. The reserved namespaces remain available and are not foreclosed: a wallet that wishes to expose policy-governed actions to listActions filtering MAY register a BRC-111 module and label such actions, and this standard neither requires nor prevents it. Nothing in the enforcement semantics of § 5 depends on a label being present, and a wallet MUST NOT treat the presence or absence of any label as evidence that a policy applies.

Specification

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as in RFC 2119.

1. Roles

  • Operator: the human wallet owner. The only party who may author, sign, or revoke a policy. All operator actions are attended (require the wallet's normal unlock/confirm).
  • Agent: autonomous software presenting a bearer token to the wallet's local interface. Untrusted; may request anything.
  • Wallet (enforcement point): holds keys, evaluates every request against the policy, performs the atomic ledger accounting, and signs/broadcasts only permitted spends.
  • Issuer: the operator's wallet that signs a PolicyRecord, making it tamper-evident and attributable to an attended operator action rather than to the agent.
  • Agent Account: a BRC-42/43-derived, isolated account funded with a capped allowance, dedicated to exactly one agent. Its balance is the hard blast-radius cap.

2. Architecture

Each agent is bound to one isolated agent account and one signed PolicyRecord. The wallet exposes a loopback (or onion-fronted) . An agent authenticates with a per-agent bearer token (Section 4) presented in the X-Agent-Token header; a valid token resolves to a binding (agent_id, account_id, allowance). A createAction carrying a valid agent token is routed to the policy-gated headless spender instead of the attended path. The wallet MUST enforce the policy for every such request (Section 5); the agent is never trusted to self-limit.

3. The signed PolicyRecord

A PolicyRecord is an immutable policy for one (agent_account, agent_identity) pair. Wherever it is stored or carried it is represented as a signed envelope. An implementation is not required to accept one over a network interface, and one that reads its envelope only from a path the operator controls is conformant: what makes an envelope trustworthy is its signature, never the channel it arrived on. The envelope is: a canonical JSON payload (the fields below) plus an issuer signature over that payload. The payload fields, their canonical destination values, and every enforcement semantic in Section 5 are normative and are implemented by the reference engine; the JSON canonicalization and BRC-3 signature envelope (Sections 3.3-3.4) are the normative interoperable serialization that makes the record tamper-evident and portable between implementations.

3.1 Payload fields

A row marked MUST is required on the wire. Where such a row also names a value, that value is what a conforming producer writes when it has no better one, and NOT a default a verifier supplies for an absent field: a payload missing any MUST field is rejected before any default could apply. The exact-fields rule of this section and the defaults are therefore not in tension.

All satoshi and time values are non-negative integers. Times are integer Unix seconds. Durations are integer seconds.

FieldTypeReq.Meaning / constraint
typestringMUSTDomain separator. MUST equal the exact literal "brc-181/agent-policy/1", which carries this standard's number zero-padded to three digits. A verifier MUST reject any other value, and the string MUST NOT change without a new standard, because every signature ever issued is bound to it.
origin_tokenstringMUSTThe agent identity this policy authorizes. MUST equal the agent_id of the bearer-token binding and the origin argument the enforcement engine receives. This is the pinned agent identifier, NOT the secret bearer token.
account_idinteger ≥ 0MUSTThe isolated agent account this policy may spend from. Its numbering is wallet-internal and carries no cross-implementation meaning, but it is not inert: an engine MUST refuse to open a ledger for this envelope against any account other than the one named here, and MUST refuse to debit any other account under it. This is the check that makes the isolation requirement of Section 10 enforceable rather than advisory. A wallet that does not number accounts MAY use any stable non-negative integer, including 0.
issuerstring (hex)MUST33-byte compressed public key (66 lowercase hex chars) that signed this payload. MUST equal the wallet's pinned issuer key (Section 3.4).
issued_atinteger ≥ 0MUSTIssuance time (epoch seconds). Used for supersession ordering (Section 9).
noncestring (hex)MUST≥ 8 bytes of issuer-chosen entropy (lowercase hex). Makes two otherwise-identical policies distinct and defeats replay of a superseded policy.
per_tx_capinteger ≥ 0MUSTMax satoshis a single action's payout may spend. 0 denies all payouts; that a zero-payout action still incurs a fee, so a wallet MUST also apply max_fee (below) rather than treating 0 as a total freeze.
period_capinteger ≥ 0MUSTMax satoshis spent (payout + fee) within the rolling period_window_s.
max_feeinteger ≥ 0MUSTMax satoshis of miner fee a single action may incur. Checks 4, 6, 7 and 9 test the payout; the fee is not known until the transaction is built, so it is bounded separately here and re-checked after build_tx (Section 5.3). 0 forbids any fee-bearing action.
period_window_sinteger ≥ 1MUSTRolling-period width in seconds.
total_budgetinteger ≥ 0MUSTLifetime allowance ceiling (payout + fee). Hard stop.
dest_allowlistarray<string>MUSTCanonical destinations (Section 3.2) permitted at the global per_tx_cap. MAY be empty (deny all un-capped destinations). Sorted ascending by Unicode code point, de-duplicated.
rate_limitinteger ≥ 0MUSTMax actions per rate_window_s.
rate_window_sinteger ≥ 1MUSTRate window width in seconds. Default 60.
expiry_epochinteger ≥ 0MUSTPolicy auto-revokes at this epoch. 0 = no expiry.
purposestringMUSTFree-form, human-readable UTF-8 shown in the audit log. MUST NOT affect enforcement. MAY be empty.
cb_burst_per_sinteger ≥ 0MUSTCircuit breaker: more than this many actions within any 1-second window trips a freeze. 0 disables.
cb_max_new_destsinteger ≥ 0MUSTCircuit breaker: more than this many first-seen destinations within the current period trips a freeze. 0 disables. A destination present in dest_allowlist or dest_caps at the time of the request MUST NOT be counted: the operator curated it into the signed grant, so it is not a novel destination and counting it would let ordinary on-policy fan-out latch a freeze only a human can clear. The breaker exists to notice destinations the policy did not anticipate.
escalate_over_per_txbooleanMUSTIf true, an over-per-tx-cap request yields ESCALATE (attended fallback) instead of REJECT. Default false (reject-fast).
escalate_off_allowlistbooleanMUSTIf true, an off-allowlist request yields ESCALATE instead of REJECT. Default false.
dest_capsarray<[string, integer≥1]>MUSTPer-destination cap overrides: [canonical_dest, tighter_per_tx_cap] pairs. A listed destination IS allowlisted, but under its own, typically tighter, ceiling. This admits a lower-trust destination (unverified contact / raw address) with a bounded blast radius. It can only TIGHTEN (the global per_tx_cap still applies first). Sorted ascending by the canonical_dest element; each canonical_dest unique.
can_create_childrenbooleanMUSTWhether this grant authorizes the agent to create child agents at all. When false an engine MUST refuse every delegation regardless of engine-side configuration, and child_budget_ceiling MUST be 0, since a ceiling on children that cannot be created states a bound an auditor would read as a grant.
delegation_depthinteger ≥ 0MUSTThis record's own depth in the delegation tree: 0 is the top-level policy-governed agent, and a child created under it carries 1. It is authority because it decides whether this record may delegate further, and an engine MUST refuse can_create_children on a record already at the deepest level it supports. Signing it is what stops an engine relocating a child to depth 0 and handing it an authority the issuer never granted.
child_budget_ceilinginteger ≥ 0MUSTMaximum total_budget any child record derived under this grant may carry. A child whose record exceeds it MUST be refused at creation, never silently clamped.

A payload MUST contain exactly these fields, no more, no fewer. An enforcement engine MUST reject a payload with an unknown field, a missing required field, a wrong JSON type, a negative numeric, a value outside the range its row states, or a type it does not recognize (fail-closed). The range clause is not redundant with the type clause: period_window_s and rate_window_s are specified as integer ≥ 1, and a signed payload carrying 0 for either is a well-typed non-negative integer that prunes its window to nothing and switches that check off, so an engine that tests only type and sign accepts a policy with two of its six enforcement axes silently disabled.

An engine MUST reject a payload whose dest_allowlist or dest_caps carries more than 1024 entries, and MUST reject one whose canonical bytes exceed 256 KiB. Both are stated as fixed numbers rather than as advice because a policy is signed: an engine that finds one too large cannot trim it and re-sign it, so a bound an implementer chooses privately is a bound at which an otherwise valid grant becomes unloadable on one engine and fine on another. A request is separately bounded: an engine MUST reject a createAction carrying more than 1024 outputs before it parses them, since Section 6 holds the per-account lock across the whole parse-and-check pipeline and an unbounded output array is therefore a denial of service against every other spend on that account.

3.2 Canonical destination format

Destination matching is performed over a canonical destination string, not over a raw locking script or an address. This closes the non-P2PKH allowlist bypass: an operator can allowlist a specific non-standard script by its hash, and any unknown script simply fails to match. Given a locking script, an engine MUST compute the canonical destination as follows.

Script normalization. The locking script MAY be presented as raw bytes, an array of byte-valued integers, a hex string, or a standard base64 string. An engine MUST normalize to raw bytes by: bytes/array → the bytes; string → attempt hex decode first, and if that fails, base64 decode. Any other form MUST be rejected as malformed.

Canonicalization. Let b be the normalized script bytes.

if len(b) == 25
 and b[0]==0x76 and b[1]==0xA9 and b[2]==0x14
 and b[23]==0x88 and b[24]==0xAC: # OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG
 canonical = "p2pkh:" + lowerhex(b[3..23]) # the 20-byte hash160
else:
 canonical = "script:" + lowerhex(SHA256(b)) # single SHA-256 of the whole script, 32 bytes

The p2pkh: form is the P2PKH hash160 (40 hex chars). The script: form is the single SHA-256 of the entire locking script (64 hex chars). An engine MUST use exactly one SHA-256 (not double-SHA-256) for the script: form.

data outputs. A data output whose script begins with bare OP_RETURN (0x6a) or with OP_FALSE OP_RETURN (0x00 0x6a) is data, not a payout: it contributes no destination and MUST NOT be matched against the allowlist.

Such an output MUST carry satoshis equal to 0, and an engine MUST REJECT a request in which any OP_RETURN-prefixed output carries a non-zero value ("value on a data output"). The value still counts toward the request total per Section 5.1, so the caps bound the amount, but the caps are not the control at issue here: an output that contributes no destination is invisible to the allowlist, to dest_caps and to the new-destination breaker of check 10, so without this rule an agent could move the whole budget to destinations the operator never approved and trip no anomaly detector. The form matters too. Post-Genesis, OP_RETURN terminates script evaluation and the outcome is decided by what the unlocking script left on the stack, so OP_FALSE OP_RETURN is provably unspendable and a bare OP_RETURN output is redeemable by whoever spends it first. Value on a bare-0x6a output is therefore not burned, it is payable to an anonymous third party, which is precisely the destination-free payout this standard exists to prevent.

3.3 Payload canonicalization (for signing and verification)

The signature preimage is the payload serialized deterministically as UTF-8 bytes:

  1. A single JSON object containing exactly the Section 3.1 fields.
  2. Object member keys sorted ascending by Unicode code point.
  3. No insignificant whitespace: , between members, : between key and value, nothing else.
  4. Integers: minimal decimal, no leading zeros (except the literal 0), no +, no exponent, no fractional part. All numeric policy fields are integers in canonical form, and every one MUST be in [0, 2^53 - 1]. The upper bound is interoperability, not thrift: a JSON parser that represents numbers as IEEE-754 doubles, which is the default in the language most BRC-100 wallets are written in, cannot round-trip an integer above 2^53, so a payload carrying one canonicalizes to different bytes on two conforming engines and its digest does not reproduce. A payload with any numeric field outside that range MUST be rejected. (Where a reference implementation stores expiry_epoch or cb_burst_per_s as a float internally, it MUST canonicalize to the equivalent integer: expiry_epoch = whole epoch seconds, cb_burst_per_s = whole actions-per-second; the enforcement comparisons are unaffected.)
  5. Booleans: true / false.
  6. Strings: RFC 8259 encoding with the mandatory escapes (\", \\, \b, \f, \n, \r, \t) and every non-ASCII code point and other control character escaped as \uXXXX with lowercase hex digits (i.e. ASCII-only output). Three further rules are REQUIRED, because each is a degree of freedom a common serializer resolves the other way and each changes the digest:
    • The solidus / is NOT escaped. Every type value contains two, so a serializer that escapes it by default produces a different digest for every policy ever signed rather than only for unusual ones.
    • A character above U+FFFF is escaped as the UTF-16 surrogate pair, two \uXXXX escapes. A six-digit or \U-style escape is not this form.
    • Strings are compared and canonicalized as the exact code-point sequence received. No Unicode normalization is applied. purpose and origin_token are the fields that carry arbitrary text, and an engine that normalized either would produce a digest the issuer's signature does not cover.
  7. dest_allowlist: array of canonical-dest strings, sorted ascending by Unicode code point, de-duplicated.
  8. dest_caps: array of 2-element arrays [canonical_dest, cap], sorted ascending by the canonical_dest element; canonical_dest values unique.

Let C be these canonical bytes. The payload digest is SHA256(C) (single SHA-256, 32 bytes).

3.4 Signature envelope

A signed policy is the JSON object:

When present, canonical MUST be a JSON string holding those bytes directly, not base64, not hex, and not an array of byte values. Rule 6 above makes the canonical bytes ASCII-only, so they are always representable as a JSON string; without this sentence a producer whose language marshals a byte array as base64 by default and a verifier that expects a literal string will disagree on a byte-perfect, correctly-signed envelope, and the mismatch rule below obliges the verifier to reject it and report a tamper failure that did not occur.

An envelope MAY additionally carry canonical, the exact byte string the producer serialized and signed. When present a verifier MUST compare it against the bytes it recomputes and reject a mismatch, which is how step 1 below detects a payload that arrived in a non-canonical form. When absent a verifier recomputes the canonical bytes and verifies against those, so a payload whose serialization was not canonical still verifies: the signature covers the canonicalized document either way, and what is lost is the ability to notice. A producer that wants its counterparty to detect a non-canonical serialization MUST emit canonical.

{ "payload": <canonical payload>, "sig": {
 "alg": "ECDSA-SHA256-secp256k1",
 "issuer": "<33-byte compressed pubkey, hex>",
 "signature": "<DER-encoded ECDSA signature, hex>" } }
  • alg MUST be "ECDSA-SHA256-secp256k1".
  • sig.issuer MUST byte-equal payload.issuer (the issuer is bound inside the signed payload as well, defeating key-substitution ambiguity).
  • signature is a DER-encoded ECDSA (r, s) over the payload digest of Section 3.3, produced with the private key corresponding to issuer. Signatures SHOULD be low-S normalized. Deterministic (RFC 6979) are RECOMMENDED.

Verification (an engine MUST, before accepting a policy):

  1. Parse the envelope; recompute the canonical payload bytes C from payload per Section 3.3 and confirm they reproduce the received serialization (reject if the received bytes were not canonical).
  2. Confirm sig.issuer == payload.issuer and that this key equals the wallet's pinned issuer key, a build-/config-pinned identity key the operator controls. An engine MUST NOT accept an issuer key supplied only in the record or by the agent; the pin is authoritative. A policy signed by any other key MUST be rejected.
  3. Verify the DER signature over SHA256(C) against issuer. Reject on failure. A verifier MUST parse the signature as DER rather than index into it: the SEQUENCE tag, the outer length, both INTEGER tags and both integer lengths MUST be checked, the outer length MUST cover exactly the remaining bytes, neither integer may carry a negative value or an unnecessary leading zero, and nothing may follow the second integer. An implementation that reads r and s from fixed offsets will accept a signature whose tags are wrong, whose declared length is wrong, or which has bytes appended, so one signature gains unboundedly many accepted encodings. Anything that treats the signature bytes as an identity, a de-duplication key or a replay marker can then be walked past using a re-encoding of a signature it has already seen.
  4. Enforce structural validity per Section 3.1.

Only a policy that passes all four steps may be loaded into an allowance ledger. This ties every autonomous-spend authority to an attended, operator-signed artifact.

3.5 Operator-side fields, and the line they MUST NOT cross

Section 3.1 fixes the signed payload exactly. An engine will nonetheless hold state about an agent that no issuer signed: a wallet-side deny-set, per-category sub-budgets, a note about who approved what. Nothing in this standard forbids that, and forbidding it would be unenforceable.

The line is authority. An unsigned field MAY narrow what the signed policy permits. An unsigned field MUST NOT grant anything the signed policy does not already permit, and MUST NOT enlarge any bound the signed policy sets. This is the same invariant Section 5.4 places on interposed checks, applied to state instead of to code, and for the same reason: the envelope is the record of what the issuer authorized, and an engine that adds authority beside it makes the signature describe less than the wallet will do.

Delegation is where this bites, and an implementation should read it carefully. If an engine lets a policy-governed agent create child agents with budgets of their own, then the authority to delegate, its depth, and the ceiling on what a child may hold are all authority. They are therefore signed fields, can_create_children, delegation_depth and child_budget_ceiling in Section 3.1, and an engine MUST read them from the verified envelope and from nowhere else. An engine that took them from operator-side configuration would have an agent minting spenders under a grant whose signed text says nothing about delegation, and an auditor reading the envelope could not see the tree it produced.

Two bounds that a child creation may carry, namely how many children and which child types, are deliberately not signed. Both can only ever narrow what the three signed fields already permit: a lower child count and a narrower type list each admit a subset of the trees the signature authorizes, so they fall under the general rule above rather than the exception. An engine MUST NOT read any unsigned field as raising delegation_depth, enabling creation when can_create_children is false, or lifting child_budget_ceiling.

A child's own record MUST be attenuated against this envelope and re-checked against it before the child may act: no child may carry a total_budget above child_budget_ceiling, a child's own signed record MUST carry a delegation_depth greater than its parent's, and a child MUST NOT itself delegate unless its own signed record says so and its depth is not already the deepest the engine supports. An implementation that supports delegation and cannot express these fields in a signed payload MUST refuse to delegate rather than delegate unsigned.

4. Authentication and origin binding (bearer token)

Autonomous requests MUST NOT be authorized on the basis of a client-supplied Originator/Origin header: such a header is forgeable, and a missing one collapses distinct agents into one shared bucket, merging budgets. Instead:

  • Each agent presents a bearer token of the form agt_ + a URL-safe base64 encoding of ≥ 32 bytes of CSPRNG entropy (the reference uses secrets.token_urlsafe(32), yielding 43 chars → 47 with prefix). The token is a high-entropy secret. It MUST NOT be derived from the request and MUST NOT be logged.
  • The token is presented in the substrate's request metadata. Over BRC-5 (HTTP) that is the X-Agent-Token header; substrates without headers (BRC-6, BRC-7) carry it in their own message envelope. This standard does not require any particular carriage. A valid token resolves to a binding (agent_id, account_id, allowance).
  • Token→binding resolution MUST be constant-time across all known tokens (e.g. hmac.compare_digest against each) so a caller cannot time-probe valid token values.
  • At mint time the engine MUST enforce binding.agent_id == allowance.policy.origin_token (defense in depth: an allowance only ever authorizes its own bound agent).
  • The origin value the enforcement engine evaluates (Section 5, check 3) is the resolved agent_id. The literal string "local-unspecified" MUST always be rejected on the autonomous path.
  • Tokens are session-scoped and held in memory only for the enabled session. revoke(token) and revoke_all() (Section 9) are instant.

A createAction that does not resolve to a valid agent-token binding MUST fall through to the wallet's normal attended path; it MUST NOT be autonomously spent.

5. Enforcement semantics, per-createAction checks

For every createAction routed to the autonomous path, the engine MUST perform the following, in this order, under the atomic ledger discipline of Section 6. The engine returns a ∈ {ALLOW_AUTO, ESCALATE, REJECT}. REJECT denies; ESCALATE diverts to the attended confirm+password path; ALLOW_AUTO proceeds to reserve/sign/broadcast.

5.1 Request parsing

Parse the request's outputs into (total, dests):

  • total = Σ satoshis over all outputs. An output with a non-integer or negative satoshis MUST cause the whole request to be rejected as malformed (REJECT).
  • dests = the canonical destination (Section 3.2) of every non-OP_RETURN output. OP_RETURN data outputs contribute to total (normally 0) but never to dests.

total is the payout used by the cap checks below. (The miner fee is added later, at debit time; see Section 6.)

5.2 Check pipeline (ordered; first failure wins)

  1. Kill switch (frozen), FIRST. If the allowance is frozen, REJECT ("frozen: <reason>"). A freeze denies everything, in-flight included.
  2. Expiry. If expiry_epoch != 0 and now >= expiry_epoch, REJECT ("policy expired").
  3. Origin binding. If origin == "local-unspecified" or origin != policy.origin_token, REJECT ("origin not bound to this agent policy").
  4. Per-tx cap. If total > per_tx_cap, → ESCALATE if escalate_over_per_tx else REJECT ("over per-tx cap").
  5. Destination allowlist. Let off = destinations in dests that are neither in dest_allowlist nor keys of dest_caps. If off is non-empty, → ESCALATE if escalate_off_allowlist else REJECT ("off-allowlist destination(s)").
  6. Per-destination cap (tighten-only). Let dcaps = the caps of those dests that carry a dest_caps entry. If dcaps is non-empty and total > min(dcaps), REJECT ("over per-destination cap"). (Because check 4 already applied the global cap, a dest_caps entry can only tighten, never loosen.)
  7. Rolling period cap. With period_spent = Σ debits in the rolling period_window_s window, if period_spent + total > period_cap, REJECT ("over period cap").
  8. Rate limit. If the count of actions in the rolling rate_window_s window >= rate_limit, REJECT ("rate limit exceeded").
  9. Total budget (hard stop). If spent_total + total > total_budget, REJECT ("total budget exhausted").
  10. Circuit breaker. If cb_burst_per_s != 0 and (burst_count + 1) > cb_burst_per_s, set frozen and REJECT ("circuit breaker: burst"). If cb_max_new_dests != 0 and (distinct_seen_dests + new_distinct_dests_here) > cb_max_new_dests, set frozen and REJECT ("circuit breaker: too many new destinations"). A tripped breaker MUST latch the freeze (subsequent requests fail at check 1) until an attended operator unfreeze.

What ESCALATE means, and what it does not. The pipeline is first-failure-wins, so an ESCALATE at check 4 or check 5 ends it: checks 5 to 10 do not run, and that includes the allowlist, the per-destination caps, the period cap, the rate limit, the total_budget this standard calls a hard stop, and both circuit breakers. An untrusted requester chooses the amount, so on a policy with escalate_over_per_tx set the requester chooses when to leave the policy engine. Three rules bound that:

  • ESCALATE is not a verdict on the request, it is a referral. It MUST NOT cause a spend. The engine MUST hold the request and MUST NOT sign or broadcast anything until an attended operator approves it.
  • An operator approval MUST re-enter the pipeline at check 1 with the approval recorded, and every remaining check MUST run against the state current at approval time. A request approved at check 4 that would fail check 7, 9 or 10 MUST still be rejected; approval lifts exactly the one bound that referred it, never the rest.
  • On approval the debit follows the ordinary ALLOW_AUTO path of Section 6. An engine that commits no debit for an approved escalation lets a policy be drained one approval at a time, and one that re-refers the same request forever never completes it.

An engine that cannot present an attended path MUST treat escalate_over_per_tx and escalate_off_allowlist as false regardless of what the policy says, and reject rather than refer. Referring to nobody is a silent allow-by-timeout in every implementation that later grows a timeout.

Reason strings. The parenthesised strings above are the normative reason for each rejection, and an engine MUST return them verbatim. They are observable to the agent and they identify which check fired, so two engines that word them differently are two engines a caller cannot write one handler against; and because the pipeline is ordered and first-failure-wins, the reason is also the only evidence available that the checks ran in the order this section specifies. An engine MAY append implementation detail after the normative prefix, separated by ": ", and MUST NOT alter the prefix itself.

If all checks pass, the engine proceeds to the ALLOW_AUTO reservation (Section 6). Note that checks 4, 6, 7 and 9 test the payout total, because the miner fee does not exist until the transaction is built. Where the builder reports a fee, that fee is folded into the debit and Section 5.3 re-runs checks 7 and 9 against payout plus fee before any counter moves, so on that path there is no budget slack: a built transaction whose fee would carry a counter past its cap is rejected, not absorbed. Where the builder reports no fee at all, the engine has nothing to fold in and the caps bound the payout only; that case, and its consequences, are set out in Section 5.3 under Builders that report no fee.

5.3 Post-build fee re-check (normative)

Checks 4, 6, 7 and 9 run before the transaction exists, so they can only test the payout. The miner fee is chosen by the builder and is not knowable at that point, yet it is spent from the same account. An engine that stops here authorizes an action whose payout is 0, passing every payout check trivially, while debiting 0 + fee, with fee sized by whoever composed the request. Output count and data size are likewise unbounded before the build.

What build_tx reports. A builder either tracks miner fees or it does not, and the two cases are not the same. A builder that does not track fees at all reports no fee, and the engine treats the fee as 0: the fee is structurally absent, not unobserved. A builder that DOES track fees reports either a definite satoshi count or an explicit UNKNOWN, and UNKNOWN is a value it MUST be able to express, because a builder that cannot read the fee of the transaction it just built has to be able to say so. An engine MUST NOT substitute 0 for UNKNOWN (check 1 below), and MUST NOT accept a reported fee that is not an integer number of satoshis: coercing one (truncating a fractional value, for instance) debits less than the transaction spends.

Therefore, after build_tx returns and before signing, still holding the Section 6 lock, the engine MUST apply these five checks, in this order:

  1. REJECT ("fee could not be determined") if the builder tracks fees and reported UNKNOWN, or reported a value that is not an integer number of satoshis. An unknown fee MUST NOT be charged as 0: fee > max_fee is false for every max_fee when fee is 0, so charging zero switches check 3 off entirely, and it does so precisely on the transactions nobody could measure. This is the fail-closed rule of Section 7 applied to the only pre-broadcast unknown that would otherwise fail open.
  2. REJECT ("negative fee rejected") if fee < 0. A negative fee is not a discount. The debit is total + fee, so a negative one CREDITS the ledger and re-opens budget that was already spent. No check below catches it: a negative fee is under every max_fee, and it makes total + fee smaller than the payout that checks 7 and 9 have already passed.
  3. REJECT ("fee cap exceeded") if fee > max_fee;
  4. re-run check 7 against period_spent + total + fee, REJECT ("period cap exceeded") on failure;
  5. re-run check 9 against spent_total + total + fee, REJECT ("total budget exhausted") on failure.

The order is normative because the reason strings are observable, and because checks 1 and 2 must precede check 3: an UNKNOWN or negative fee would otherwise be compared against max_fee as though it were a satoshi count.

A rejection here MUST release the coin reservation and MUST NOT debit, by the same full-undo path Section 7 specifies for pre-broadcast failure. Only after all five pass may the engine debit and sign.

Builders that report no fee. The five checks above bound a fee the builder reported. A builder that reports none gives the engine nothing to bound, and this is the one path on which a miner fee is absorbed rather than rejected: the wallet pays it from the account, no counter moves for it, and max_fee, the period cap and the total budget therefore do not apply to it. The gap is not a small residue on the last spend before a hard stop: it is every fee on every spend, and it is bounded only by the account's balance. It is permitted because a caller that genuinely does not track fees is a legitimate integration and forcing it to invent a number would be worse. Implementers MUST NOT read this as an alternative to reporting UNKNOWN: reporting no fee says this builder never tracks fees, a property of the integration, while UNKNOWN says I track fees and could not read this one, a property of one transaction, and only the second is denied. An engine SHOULD make the distinction visible to the operator, since a policy carrying a max_fee and a builder reporting no fee is almost always a wiring mistake rather than a choice.

The verdict-to-severity mapping (ESCALATE vs REJECT) is governed solely by escalate_over_per_tx and escalate_off_allowlist. The RECOMMENDED default for high-frequency agents is reject-fast (both false): anything off-policy is instantly rejected so the agent never stalls behind an attended prompt.

5.4 Implementation-specific checks, and the one rule they MUST obey

An engine MAY interpose additional checks in the pipeline of Section 5.2, and a useful one will. The reference interposes several: an operator-only destination deny-list, per-category sub-budgets with their own rolling windows, and a separate pipeline in front of the ledger for delegated child accounts.

The invariant is that an added check MAY refuse a request the signed policy would have allowed, and MUST NOT admit one the signed policy would have refused. A policy is a grant with an upper bound. An operator who tightens their own grant has narrowed a thing they own; an engine that widens it has spent authority nobody signed for, and the signed envelope stops describing what the wallet will do. Every interposed check must therefore be expressible as an additional reason to say no.

Two consequences an implementer should read carefully, because both are reachable:

  • A second admission path for destinations is a widening unless all four conditions below hold. The hazard is concrete: if an engine admits a destination because some wallet-side set vouches for it, then with an empty dest_allowlist it will allow a payment the policy admits nowhere, and the allowlist stops being the boundary the issuer signed. A wallet-side set of trusted destinations is a legitimate thing to keep, and one case is narrow enough to admit: an identity already named in dest_allowlist has rotated to a new address and has proved control of it to the wallet. An engine MAY admit such a destination if and only if all four of these hold, and MUST NOT otherwise: the set is computed wallet-side and no part of the request can add to it or influence it; every member is an address the wallet has itself verified as controlled by an identity that is already in dest_allowlist; the set admits nothing while dest_allowlist is empty; and each admitted destination is bounded by the same per-destination cap that would apply to the allowlist entry it was derived from. Those four are what keep this a resolution of an allowlisted identity rather than a second allowlist. An engine that admits on any looser basis has widened the grant and is not conformant. Otherwise a wallet-side set belongs on the operator's side of the signature, curating what goes INTO dest_allowlist before signing, which is what Section 8 describes.

  • Enforcing a cap other than the signed one is a widening whenever it is looser, and a silent change of meaning even when it is tighter. An engine that scales the signed caps by a trust factor during a warmup period is enforcing numbers the issuer did not sign. If the factor is at most one it never widens, so it satisfies the invariant, but a reader of the envelope can no longer predict the wallet's behaviour from the envelope. An engine that does this MUST report the effective cap alongside the signed one wherever it reports a refusal, so the difference is visible rather than inferred from a rejection that does not match the arithmetic.

5.5 Every rejection is a verdict, never an exception

An engine parses an untrusted request before it can check anything, and parsing fails in ways the checks never see: a locking script that is not hex, an output with no locking script at all, a satoshis member that is a JSON boolean rather than a number. Section 7 already says a malformed request is rejected, and this is what that requires of an implementation.

An engine MUST convert every parse failure into the same REJECT verdict the checks produce, and MUST NOT allow an exception to escape the authorization call. A caller receives a verdict or it receives nothing, and nothing is indistinguishable from a crash. In a server the difference is visible: an escaping exception drops the connection without a reply, so an agent cannot tell a refusal from a fault and a retry loop cannot tell whether retrying is safe.

Three cases are named because each has a plausible wrong answer:

  • A JSON boolean is not an amount. In several languages a boolean is a subtype of integer, so true passes a naive integer test and is counted as one satoshi against every cap. Reject it.
  • A missing amount is not zero. An output with no satoshis member contributes nothing to the total while its destination still faces the allowlist and breaker checks, so a request can pass a budget it never consumed. Reject the request rather than defaulting the member.
  • A malformed header is not an absent header. A token that cannot be decoded MUST take the same path as a token that is wrong, which Section 4 already fixes as rejection. It MUST NOT take the path of a request that carried no token at all, and MUST NOT fault.

5.6 A control that needs a timer MUST name what starts it

Two of this standard's fail-closed rules describe a state that is entered on one event and left on another: a reservation is held when a broadcast outcome is uncertain and released when the outcome is reconciled, and a freeze latches until an operator clears it. The first half of each is driven by the request that caused it. The second half is driven by nothing unless an implementation arranges it.

An implementation that holds state pending reconciliation MUST name the mechanism that performs the reconciliation, and that mechanism MUST run without an operator remembering to run it. A held reservation with no driver is not a conservative default. It is a leak: coins stay reserved, the next legitimate request finds nothing spendable, and the failure surfaces as an apparently empty wallet rather than as the stuck reservation it is. An operator debugging that message looks in exactly the wrong place.

Where an implementation cannot guarantee the driver runs, it MUST make the held state visible: a reservation older than the reconciliation window MUST be reported by whatever the operator inspects for health, so a control that has stopped running is distinguishable from one with nothing to do.

5.7 Audit isolation covers every path, including the ones that fail early

Section 7 requires that a failing audit sink never change a spend decision. An implementation that routes its ordinary verdicts through an isolating wrapper will still let a sink exception escape from the paths that return before reaching it, and a malformed request is exactly such a path: it is rejected early, so it never reaches the wrapper the normal path relies on.

Audit isolation MUST hold on every path that can emit an audit record, and a path that fails before the checks run is one of them. The rule of Section 5.5 applies here too: the caller receives a verdict, never an exception, and least of all an exception raised by logging.

5.8 A caller may tighten a ceiling for one request

An engine MAY accept a per-request ceiling from its caller and apply the tighter of that and the signed one. This narrows a single request and never widens it, so Section 5.4 permits it, but the number a refusal is measured against is then not the number in the envelope. An engine that does this MUST report which ceiling refused the request, because an operator reconciling a refusal against the signed policy will otherwise find arithmetic that does not add up.

6. Atomic allowance ledger and concurrency safety

The engine MUST maintain, per (agent_account, origin), a thread-safe allowance ledger guarded by one lock. The critical section that lock protects is the entire check→coin-select→reserve→debit sequence. A language runtime's global lock (e.g. the Python GIL) does NOT make a multi-step check-then-debit atomic; without one explicit lock, two concurrent requests can both pass a cap check and then both debit (overspending) or both select the same coin (double-select). Implementations MUST hold the lock across:

  1. the full Section 5 check pipeline,
  2. coin selection + reservation (the build_tx callback, which excludes already-reserved coins and reserves the chosen ones), and
  3. the counter debit.

Signing and broadcasting (network-blocking) MUST happen outside the lock, so concurrent high-frequency spends are not re-serialized on the network path.

Debit. On ALLOW_AUTO the engine computes debit = total + fee, where fee is the miner fee the builder reported for the built transaction, so a reported fee cannot escape the caps. Where the builder reports no fee, fee is 0 and the caps bound the payout alone (Section 5.3, Builders that report no fee). It then, atomically under the lock: increments spent_total by debit; appends (seq, now, debit) to the rolling-period window; appends (seq, now) to the rate window; appends now to the burst window; and records each destination as seen. seq is a per-ledger monotonic id. The engine returns a Reservation carrying (debit, tx, reserved_coins, seq).

Ledger counters.

CounterContentPruned when
spent_totallifetime debited satoshis (seeded per Section 8)never (only rolled back)
period windowdeque of (seq, ts, debit)now - ts > period_window_s
rate windowdeque of (seq, ts)now - ts > rate_window_s
burst windowdeque of tsnow - ts > 1.0s
seen destinationsdeque of (ts, canonical_dest) + a setnow - ts > period_window_s (set entry dropped only when no recent duplicate remains)

Durability. Every counter in the table above, and the freeze flag of Section 9, MUST survive a restart of the enforcing process. Only spent_total can be rebuilt from outside (Section 8 re-seeds it from chain-observed history); the period, rate and burst windows, the seen-destination set and a latched freeze exist nowhere but in the engine, so an engine that holds them only in memory enforces period_cap and rate_limit per uptime rather than per window. Worse, it treats a process restart as the attended operator unfreeze that check 10 requires. An engine that cannot persist them MUST refuse to open a ledger rather than open one whose limits a restart resets. This is the same requirement, for the same reason, that Section 9 places on supersession state: the attack is a restart.

Clock. All window arithmetic is in seconds with a resolution of at least one millisecond; the burst window's 1.0s width is not a hint that integer seconds suffice. Window comparisons MUST use a monotonic clock, not wall-clock time, so that an NTP step or a VM restore cannot empty a window or un-expire a policy. expiry_epoch and issued_at are wall-clock epoch values by definition and are exempt.

Commit / rollback (identity-based). After signing+broadcast, the caller settles the Reservation exactly once:

  • commit(txid) marks it settled; counters stay debited.
  • rollback(reason) re-credits spent_total by debit and removes this reservation's own period-, rate-, burst- and seen-destination entries by seq, never by popping the last-appended entry. Under concurrency the last entry belongs to a different in-flight spend; popping it would leak this spend's period/rate slot and let the caps be bypassed. Every window entry an engine appends MUST therefore carry the reservation's seq, including the burst window and the seen-destination set, which are otherwise not addressable and would leave a never-broadcast spend counting permanently toward both circuit breakers.

A settled reservation MUST ignore a second commit/rollback (idempotent).

7. Fail-closed rules

An engine MUST implement all of the following:

  • noSend denied. A createAction with noSend (sign-and-store without broadcast) MUST be rejected on the autonomous path. A stored-but-unbroadcast spend leaves coins reserved with nothing on-chain to reconcile against, an unbounded reservation and double-select hazard. Agents have no reason to pre-sign.
  • sendWith denied. A createAction carrying BRC-100's options.sendWith MUST be rejected on the autonomous path ("sendWith on the autonomous path"). sendWith names transactions created earlier as noSend actions and broadcasts them alongside the new one. Denying noSend closes only their creation; without this rule an agent submits an action whose own outputs are trivial, passes every check in Section 5.2 on those trivial outputs, and causes arbitrary pre-signed value to be broadcast beside it at zero policy cost, because Section 5.1 derives total and dests from outputs alone and never sees the batch.
  • Delayed broadcast denied. The engine MUST set BRC-100's options.acceptDelayedBroadcast to false for every request on the autonomous path, and MUST reject a request that asks for true ("delayed broadcast on the autonomous path"). Its BRC-100 default is true, under which the transaction is handed to a background process; there is then no synchronous broadcast at which the freeze re-check below can run, and no return value whose absence the broadcast-uncertainty rule can read. Both of those rules are stated as MUSTs in this section and neither is satisfiable under a delayed broadcast, so an engine that leaves the default in place is not conformant however carefully it implements the rest.
  • Completeness assertion. Before broadcast the engine MUST assert the signed transaction is_complete(). An incomplete signature is a pre-broadcast failure.
  • Pre-broadcast failure ⇒ full undo. Any failure before broadcast (policy denial, coin-selection failure, sign error, incomplete tx) MUST roll the counters back AND release the reserved coins. Nothing left the wallet, so a full undo is safe.
  • Freeze re-check before broadcast. After signing and immediately before broadcast (the last point at which the spend is still safely reversible) the engine MUST re-check the kill switch. A freeze that landed mid-sign MUST stop the spend (rollback + release).
  • Broadcast uncertainty ⇒ fail CLOSED. A broadcast that raises OR returns no txid is ambiguous: the transaction may already be propagating. The engine MUST treat it as possibly-on-chain: keep spent_total debited, keep the coins reserved (never re-credited/released), commit the reservation, and surface a distinct ERR_BROADCAST_UNCERTAIN. Re-crediting here would let a concurrent spend re-select a coin that is actually confirming, a double-spend. Reconciliation against chain state resolves the true outcome before any retry.
  • Reservation held until reconciled. On success the coin reservation is held until the spent coin is reconciled out of the account's live set; only then is the reservation dropped. A concurrent build MUST NOT be able to re-select a just-spent coin.
  • Audit isolation. Audit/observability is best-effort. A throwing audit sink MUST NOT corrupt the ledger, unwind a fund-state mutation, or turn a successful spend into an exception (which a caller could read as failure and retry, double-spending). Audit failures MUST be isolated and surfaced loudly (e.g. to stderr), never swallowed silently and never fatal to the spend.
  • Malformed request ⇒ REJECT. Any parse/validation failure in Section 5.1 is a REJECT, never a best-effort spend.

8. Budget seeding and graduated-trust destinations

Chain-authoritative seed. At startup the ledger's spent_total MUST be seeded from chain-observed history, not solely from a local counter, so that a wiped or edited local counter cannot re-open the budget. The is the account's net outflow (sum of negative history value-deltas). This is deliberately conservative (it counts fees and full outflow and ignores returned change) so it can only under-credit remaining budget, never re-open it.

Graduated trust → graduated caps. A curation layer produces dest_allowlist and dest_caps from an operator-selected subset of contacts, enforcing three properties:

  • Selective disclosure: only the selected destinations are exposed to the agent; the rest of the address book stays private.
  • Verified → global cap; unverified/raw → own tighter cap. A verified contact goes on dest_allowlist at the global per_tx_cap. An unverified contact or a raw address is admitted ONLY via dest_caps under its own, strictly positive, tighter per-tx ceiling, bounding a lower-trust destination's blast radius below everything else.
  • Explicit opt-in (fail-loud). Admitting a lower-trust destination MUST require both an explicit risk-acknowledgement flag and a positive per-tx cap, or the operation fails loudly. A lower-trust destination can never be admitted by accident.
  • Resolve-then-pin. A / MUST be resolved to an address once, at selection time, and the resulting address pinned; the agent MUST NOT re-resolve per spend (a hijack surface).

9. Revocation and supersession

One envelope authorizes one ledger. An engine MUST NOT open a second allowance ledger from an envelope it has already accepted. Caps bound spending only while one counter stands behind them: two ledgers built from one envelope are two counters, each permitting the full signed total, and every per-ledger check passes because each ledger really is within the signed caps. Nothing in the caps can detect the doubling, so the rule has to live at acceptance. An issuer who wants a second ledger signs a second envelope with a fresh nonce, which is a deliberate act that leaves a record.

Supersession needs durable state, and an engine without it cannot claim this clause. The ordering rule is only enforceable if a candidate policy can be compared against something that survives a restart, because the attack IS a restart. An engine holding the current issued_at only in memory accepts an older envelope on the next start, restoring the destinations and the looser caps a newer policy had narrowed, with a signature that verifies perfectly and no cap check that can object. Reconciling spend from chain history does not close this: that restores what an agent has SPENT, never the policy's TERMS.

An implementation SHOULD take the durable part from state it already keeps rather than adding a store for it. A log that a compartment already replays to seed its spent totals can carry the policy revision on the same records and return it through the same seeding path, which introduces no new way for a legitimate restart to fail: a log readable enough to seed a budget is readable enough to seed a revision. An implementation with no such state MUST say so rather than present the ordering rule as enforced.

An implementation will usually have more ways to stop an agent than this section lists: a token that expires on its own, a token rotated out from under a binding, a dead-man timer, an account retirement. Any of these is a legitimate additional way to say no, and Section 5.4's rule covers them all. What matters for interoperability is the converse, so it is stated once: every mechanism that stops authority MUST be observable to the operator, and none of them may be the only thing standing between an agent and a spend. An engine whose sole protection against a stale grant is a token has a policy that outlives its own enforcement.

The engine MUST support all of:

  • Freeze / unfreeze. freeze(reason) immediately denies everything (check 1); in-flight actions fail closed. unfreeze is an attended operator action only.
  • Token revocation. revoke(token) instantly deauthorizes one agent; revoke_all() drops every token (session teardown / global kill switch). A revoked token no longer resolves to a binding, so its requests fall off the autonomous path.
  • Total revocation. The composite operator action drops the token(s) AND freezes the allowance.
  • Expiry. A non-zero expiry_epoch auto-revokes at that time (check 2).
  • Supersession. Policies for the same (account_id, origin_token) are totally ordered by the pair (issued_at, nonce), comparing issued_at as an integer first and, when those are equal, the nonce strings by byte-wise lexicographic comparison of their lowercase hex. A newly loaded, validly-signed policy replaces the prior one only if it is strictly greater under that order, and an engine MUST NOT load one that is less than or equal to the policy currently loaded (replay defense). Equality is included deliberately: a rule written only against a lower issued_at leaves two policies issued in the same second freely interchangeable, including the looser of the two, and one-second granularity makes that the ordinary case for any scripted re-issue rather than an edge case. An issuer MUST NOT reuse a nonce for the same (account_id, origin_token); two records identical in both fields are the same record, and an engine that receives a second one differing in any other field MUST reject it rather than choose between them. Because policies are session-loaded and issuer-pinned, and the budget seed is chain-authoritative, a revoked or superseded policy cannot re-open spent budget.

10. Security requirements (summary of MUSTs)

  • The wallet MUST enforce the policy; no field of the agent's request is trusted except as input validated against the policy.
  • Autonomous authority MUST be scoped to an isolated, capped account, never the main balance or seed.
  • Destination matching MUST be over canonical destinations (P2PKH hash160 + script-hash), closing the raw-script bypass.
  • The check→reserve→debit critical section MUST be atomic under one per-account lock; signing/broadcast MUST be outside it.
  • The post-build fee re-check (Section 5.3) MUST run after build_tx and before signing, and it is five checks in this order, not three: a fee the builder could not determine ("fee could not be determined"), a negative fee ("negative fee rejected"), fee > max_fee ("fee cap exceeded"), and a period cap or total budget that payout plus fee would carry past its ceiling ("period cap exceeded", "total budget exhausted"). Each MUST REJECT, release the coin reservation and debit nothing. Without the re-check at all, the cap checks bound the payout only, and a zero-payout action clears every one of them while still spending a fee. Without the first two, an unknown fee charged as 0 clears max_fee for every ceiling, and a negative fee credits the ledger and re-opens spent budget.
  • Rollback MUST be identity-based (seq), never pop-last, and MUST cover every window the request touched, the burst window and the seen-destination set included.
  • An OP_RETURN-prefixed output MUST carry zero satoshis, and a request in which one carries value MUST be REJECTED. It contributes no destination, so it is invisible to the allowlist, to dest_caps and to the new-destination breaker, and on a bare 0x6a output the value is spendable by anyone rather than burned.
  • sendWith and acceptDelayedBroadcast MUST both be refused on the autonomous path. The first broadcasts value the check pipeline never sees; the second removes the synchronous broadcast that the freeze re-check and the broadcast-uncertainty rule both require in order to run at all.
  • Every ledger counter and the freeze flag MUST survive a restart. Only spent_total can be rebuilt from chain state, so an engine holding the rest in memory enforces the period and rate caps per uptime and treats a restart as the attended unfreeze a tripped breaker requires.
  • ESCALATE MUST NOT cause a spend. It ends the pipeline, so an approval MUST re-enter at check 1 and run every remaining check against current state; an engine with no attended path MUST reject rather than refer.
  • An engine MUST refuse to open a ledger for an envelope against any account other than its signed account_id, and MUST refuse to debit any other account under it. This is what makes the isolation requirement above enforceable rather than advisory.
  • Policies MUST be totally ordered by (issued_at, nonce) and an engine MUST NOT load one less than or equal to the policy currently loaded. A rule written only against a lower issued_at leaves two policies issued in the same second interchangeable, including the looser one.
  • The three delegation authorities MUST be read from the verified envelope and from nowhere else. No unsigned field may raise delegation_depth, enable creation when can_create_children is false, or lift child_budget_ceiling.
  • Broadcast uncertainty MUST fail closed; the kill switch MUST be re-checked at the last safe point pre-broadcast.
  • A policy MUST verify under the wallet's pinned issuer key before it may authorize any spend.
  • Bearer-token resolution MUST be constant-time across the whole set an attacker can probe. Comparing each candidate in constant time is not sufficient on its own: if the tokens are spread over several registries and a request reaches them in sequence, the number of comparisons performed before a match still varies with the token presented, and that count is observable. An implementation that partitions its tokens MUST either search every partition on every request or ensure the partition is not selected by attacker-controlled input. Tokens MUST NOT be logged, which includes writing one to standard output in a deployment whose standard output is collected by a log.

Test Vectors

Vector A, signed PolicyRecord (metered endpoint, stable payee address)

Issuer test key: secp256k1 private scalar 424242 (a throwaway test key; NOT an operator key), whose compressed public key is 03e9a2463c5ecaaaac49dc3ac382cae02cec513d342ee9a6c18e842c344f7b2bfb.

On the type domain separator. These payloads are signed over brc-181/agent-policy/1, so an implementer can hash and verify them exactly as printed. The number is inside the signed payload rather than beside it, so it is not cosmetic: were this standard reassigned a different number, every digest and signature in this section would change with it. They are re-derived by tools/brc_agent_policy_vectors.py --number <N> --write, never edited by hand.

Canonical payload bytes C (exactly, no trailing newline):

{"account_id":7,"can_create_children":false,"cb_burst_per_s":0,"cb_max_new_dests":0,"child_budget_ceiling":0,"delegation_depth":0,"dest_allowlist":["p2pkh:1111111111111111111111111111111111111111"],"dest_caps":[],"escalate_off_allowlist":false,"escalate_over_per_tx":false,"expiry_epoch":0,"issued_at":1752710400,"issuer":"03e9a2463c5ecaaaac49dc3ac382cae02cec513d342ee9a6c18e842c344f7b2bfb","max_fee":100,"nonce":"a1b2c3d4e5f60718","origin_token":"agt-marketplace-bidder-01","per_tx_cap":100,"period_cap":1000000000,"period_window_s":3600,"purpose":"per-query metering, payee at a stable address","rate_limit":1000000,"rate_window_s":60,"total_budget":1000,"type":"brc-181/agent-policy/1"}

Expected outputs:

  • Payload digest SHA256(C) = 60ce6f775987fcf1d7291a67f20c46fbb6ff09147cdbb30660617badf48e8882
  • DER signature (hex) = 3044022025837bf6d69dec70d1bcfc715f939fbe4150c26b403455ac824f334cd28e956e02206cfd2b1f088eaf03321eb2a6f1964cbd2958e80dcc9700ae2ed89377009c0320
  • Verifying the DER signature over SHA256(C) against the issuer public key MUST succeed. A signer using RFC 6979 deterministic nonces will reproduce this exact signature; any other valid signature is also acceptable, but the digest above MUST match.

Vector B, signed PolicyRecord with per-destination cap + circuit breaker

Same issuer key as Vector A. This policy allowlists one verified P2PKH destination at the global cap and admits one lower-trust script destination under a tighter dest_caps ceiling of 5000, with both circuit breakers armed.

Canonical payload bytes C:

{"account_id":7,"can_create_children":true,"cb_burst_per_s":3,"cb_max_new_dests":2,"child_budget_ceiling":250000,"delegation_depth":1,"dest_allowlist":["p2pkh:1111111111111111111111111111111111111111"],"dest_caps":[["script:76649d1560a19192282962c83a535b543b544dcf4da3d7ef89d428083435479f",5000]],"escalate_off_allowlist":false,"escalate_over_per_tx":false,"expiry_epoch":0,"issued_at":1752710400,"issuer":"03e9a2463c5ecaaaac49dc3ac382cae02cec513d342ee9a6c18e842c344f7b2bfb","max_fee":5000,"nonce":"0f0e0d0c0b0a0908","origin_token":"agt-marketplace-bidder-01","per_tx_cap":100000,"period_cap":1000000000,"period_window_s":3600,"purpose":"pay curated contacts, bounded","rate_limit":1000000,"rate_window_s":60,"total_budget":1000000000,"type":"brc-181/agent-policy/1"}

Expected outputs:

  • Payload digest SHA256(C) = eab3434ad70de51918c2e23fa92972960508bdb1bdb064d77330bc23494cd93f
  • DER signature (hex) = 30440220362cbecaa6645a65f5ea433c82c9c188ab0ee2bd42501c6870ece9be775cb89c02203cb1d02b7878611d05330d4c39353326605b633e82cef0411dc4d8dd8a2dce5f
  • Signature verification against the issuer key MUST succeed.

The script: destination above is derived per Section 3.2 from the non-P2PKH script 52210300ff210300ee52ae: SHA256(52210300ff210300ee52ae) = 76649d1560a19192282962c83a535b543b544dcf4da3d7ef89d428083435479f.

Vector C, canonical destination computation

Input locking script (hex)Expected canonical destination
76a914 + 11×20 + 88ac (P2PKH)p2pkh:1111111111111111111111111111111111111111
52210300ff210300ee52ae (bare multisig-ish, non-P2PKH)script:76649d1560a19192282962c83a535b543b544dcf4da3d7ef89d428083435479f
52210300aa88ac (non-P2PKH, wrong length)script:a0d411a4e1302e346978e9d905c9819548530cdba29850942e827aab8538303b
6a04deadbeef (OP_RETURN data)none, contributes no destination

The P2PKH input MUST also produce the same canonical value when presented as raw bytes or as an array of byte integers (script normalization, Section 3.2).

Vector D, enforcement walk (runtime)

Policy: origin_token="agent1", per_tx_cap=100, period_cap=1_000_000_000, period_window_s=3600, total_budget=1000, dest_allowlist={p2pkh:1111…1111}, rate_limit=1_000_000, rate_window_s=60, max_fee=100, all breakers/escalations off. Coin selection returns a fresh distinct coin each call.

This walk is stated at zero miner fee, so the arithmetic below isolates the payout path. Fees are not ignored by the model: per Section 6 the debit is payout + fee, so at a non-zero fee the same policy admits fewer than ten actions before total_budget is reached. An implementation MAY run the walk at a non-zero fee. The result then has two regimes, divided by this policy's max_fee of 100:

  • fee <= max_fee. Row 6 yields exactly floor(1000 / (100 + fee)) approvals, and spent_total settles at exactly approvals * (100 + fee). At fee = 0 that is 10 approvals and spent_total = 1000; at fee = 100 it is 5 approvals and spent_total = 1000.
  • fee > max_fee. Row 6 yields 0 approvals and spent_total stays 0. At fee = 150 the formula above would give 4; the conforming answer is 0.

The formula therefore describes the budget regime only, and an engine that returns 4 at fee = 150 is not applying max_fee. Both regimes state the counts and totals only. They deliberately name no mechanism: which check rejects a given request depends on the fee, and an implementation conforms by producing these numbers, not by producing them in a particular way.

#RequestExpected verdictReason
1pay 100 → 76a914+11×20+88acALLOW_AUTOwithin policy; spent_total → 100
2pay 101 → allowlisted P2PKHREJECTover per-tx cap (100)
3pay 50 → 76a914+22×20+88acREJECToff-allowlist destination
4pay 50 → 52210300ff210300ee52aeREJECTnon-P2PKH raw script not allowlisted (bypass closed)
5origin local-unspecified, pay 1 → allowlistedREJECTorigin not bound
6300 requests each pay 100 → allowlisted P2PKH, run sequentially and again concurrentlyexactly 10 ALLOW_AUTO, rest REJECTbudget 1000 / per-tx 100 = 10; spent_total settles to exactly 1000 under either ordering; the no-coin-double-selected clause is the half only a concurrent run can exercise, and an implementation whose selector yields inside the critical section MUST still satisfy it

Graduated-trust sub-case (per Section 8): with per_tx_cap=100000 and a dest_caps entry (scriptDest, 5000) for an unverified destination, a request of 5000 to that destination is ALLOW_AUTO; 5001 is REJECT ("over per-destination cap"); and if the dest_caps entry were 999999 while the global per_tx_cap were 10000, a request of 20000 is REJECT ("over per-tx cap") because the global cap is checked first (a per-destination cap can only tighten).

Rollback sub-case (identity-based, Section 6): two in-flight reservations A (amount 100, seq 0) and B (amount 30, seq 1); period_spent = 130. Rolling back A MUST leave period_spent = 30 and spent_total = 30 (A's own window entry removed by seq, not the last-appended B entry), and B MUST still commit cleanly.

Vector E, negative (tamper detection)

A vector set that only demonstrates success cannot distinguish a correct verifier from one that returns true unconditionally. This vector fails, and an implementation that accepts it is broken in the way that matters most.

Take Vector A and raise per_tx_cap from 100 to 101. That is a one-byte edit, and the single most attractive change an attacker could make to a policy. Every other byte, including Vector A's signature, is unchanged.

Mutated canonical payload bytes C':

{"account_id":7,"can_create_children":false,"cb_burst_per_s":0,"cb_max_new_dests":0,"child_budget_ceiling":0,"delegation_depth":0,"dest_allowlist":["p2pkh:1111111111111111111111111111111111111111"],"dest_caps":[],"escalate_off_allowlist":false,"escalate_over_per_tx":false,"expiry_epoch":0,"issued_at":1752710400,"issuer":"03e9a2463c5ecaaaac49dc3ac382cae02cec513d342ee9a6c18e842c344f7b2bfb","max_fee":100,"nonce":"a1b2c3d4e5f60718","origin_token":"agt-marketplace-bidder-01","per_tx_cap":101,"period_cap":1000000000,"period_window_s":3600,"purpose":"per-query metering, payee at a stable address","rate_limit":1000000,"rate_window_s":60,"total_budget":1000,"type":"brc-181/agent-policy/1"}

Expected outputs:

  • Mutated payload digest SHA256(C') = 133f839214bff451e3ea39eed0ae8c95c74dbe965fb3bf39f981e8358f498efe, which differs from Vector A's digest.
  • Verifying Vector A's DER signature over SHA256(C') against the issuer public key MUST fail. An engine that returns any verdict other than a rejection here MUST be treated as non-conforming.
  • The engine MUST reject at signature verification and MUST NOT reach any of the Section 5 checks. Rejecting later, for instance by noticing the amount at the cap check, is not equivalent: the signature is what binds the whole payload, and a verifier that admits the record and relies on downstream checks will accept every field the checks do not read.

Two further payloads MUST be rejected without reaching signature verification at all, per Section 3.1's exact-fields rule: Vector A with any field removed, and Vector A with an unrecognized field added. Both are malformed, not merely unauthorized.

Implementations

A reference implementation exists (not a public path):

  • Policy engine + ledger: PolicyRecord, canonical_dest, amount_and_dests, and the thread-safe AgentAllowance (single per-account lock; the ordered check pipeline of Section 5; the period/rate/burst/seen-dest counters; identity-based Reservation commit/rollback; chain-seeded spent_total; freeze + circuit breaker). A 300-thread test asserts that the allowance settles to exactly its budget and that no coin is double-selected. Measured honestly, that test does not by itself prove the locking: with a microsecond critical section the threads do not overlap, and the assertions still hold if the per-account lock is removed. Under an artificially contended selector the lock is observably serializing (peak simultaneous occupancy of the critical section falls from tens to one). Implementers should treat the lock as a requirement of Section 6 rather than as something the reference test demonstrates.
  • Headless spender: the GUI-agnostic orchestration that runs policy-gated build→sign→broadcast, implementing the fail-closed rules of Section 7 (noSend denial, completeness assertion, pre-broadcast full-undo, freeze re-check, broadcast-uncertainty fail-closed, held reservations, audit isolation) via injected wallet operations, so it is unit-testable without funds and wallet-version-agnostic.
  • Bearer-token registry: per-agent unforgeable tokens, constant-time resolution, instant revocation (Section 4/9).
  • Graduated-trust contact curation: verified/unverified/raw tiers producing dest_allowlist + dest_caps with explicit opt-in and resolve-then-pin (Section 8).
  • Server routing: a BRC-100 createAction carrying a valid X-Agent-Token is resolved to a binding and routed to the policy-gated headless spender before the ordinary per-origin path.

The enforcement core is covered by a unit suite (including real parallel-thread concurrency tests) and has passed a testnet validation of the full sign/broadcast seam. The signed-envelope serialization (Sections 3.3-3.4) is the interoperable wire format specified here; an implementation MAY hold the policy in memory sealed at rest by the wallet's account registry, provided any policy it accepts over the wire verifies per Section 3.4.

Mathematical and cryptographic basis

The authority to spend reduces to one signature verification. A PolicyRecord is accepted only if its canonical payload C verifies under the wallet's pinned issuer key, over the digest SHA-256(C). Canonicalization (Section 3.3) is injective on the field set: sorted keys, tight separators, integer normalization, and sorted de-duplicated destination arrays send each distinct policy to one preimage string. To make the engine accept a policy the operator never signed, an attacker must produce a valid ECDSA signature over SHA-256(C') for a chosen C' under the pinned public key. That is an existential forgery against ECDSA on secp256k1, whose hardness rests on the elliptic-curve discrete-logarithm problem, composed with the second-preimage resistance of SHA-256. Both are infeasible, so a forged policy is infeasible.

Two design choices tighten the reduction. The issuer key is build-pinned rather than read from the record, so key substitution (signing C' under an attacker key and presenting that key alongside it) is rejected before any signature check runs. Supersession is ordered by issued_at with a nonce tie-break, a strict total order that forbids loading an earlier policy over a later one, so a captured old policy cannot be replayed to reopen spent budget. The spend limits themselves are not cryptographic. They are integer-arithmetic invariants (Section 6) maintained inside one lock, so their guarantee is atomic accounting under concurrency, distinct from and composed with the signature guarantee above.

Security Considerations

  • The agent is untrusted. The entire design assumes the agent will, at some point, be prompt-injected into requesting an attacker-controlled spend. Safety derives from wallet-side enforcement plus account isolation, never from agent behaviour. A reviewer should verify that no code path trusts an agent-supplied field except as validated input.
  • Isolation is most of the safety. The agent account holds only its allowance; even a total compromise of the held signing secret unlocks only that capped account. Operators SHOULD fund it conservatively and top it up rather than pre-loading a large balance.
  • TOCTOU / overspend. The single per-account lock over check→reserve→debit is load-bearing. A runtime-level lock (GIL) is insufficient. Dropping the lock, or debiting outside it, reintroduces overspend and coin double-selection. The identity-based (seq) rollback is equally load-bearing: a pop-last rollback silently leaks period/rate slots under concurrency.
  • Broadcast is not atomically observable. Treating an ambiguous broadcast as failed (re-crediting/releasing) risks re-selecting a coin that is actually confirming, a double-spend. This BRC mandates fail-closed with a distinct error and out-of-band reconciliation. Retrying an ERR_BROADCAST_UNCERTAIN without reconciliation is unsafe.
  • Fee escape. The Section 5.2 cap checks are on the payout, because the miner fee does not exist until the transaction is built. A payout of 0 therefore clears every one of them while the action still spends fee from the account, and per_tx_cap = 0 does not close that: it denies payouts, not fee-bearing actions. Section 5.3 is what closes it. After build_tx and before signing, the engine bounds the fee by max_fee and re-runs the period cap and the total budget against payout plus fee, releasing the coin reservation and debiting nothing on failure. An implementation that omits the post-build re-check has a fee escape bounded only by whoever composes the request.
  • Fee escape, the residue. Section 5.3 closes the escape only for fees the builder reports. Where the builder reports no fee (Section 5.3, Builders that report no fee), every fee it pays is outside the ledger and outside max_fee, the period cap and the total budget, bounded only by the account's balance. That is a deliberate accommodation for callers that do not track fees, and it is also the configuration most likely to be reached by accident: a policy that sets a max_fee while its builder reports nothing looks enforced and is not. Reviewers SHOULD check which of the two a deployment is actually on, because the difference is invisible from the policy record alone.
  • Two seams, one rule for what a fee is. An engine that accepts a fee from more than one place (a builder's return value, a wallet hook, a plugin) MUST apply the same test for "this is a satoshi count" at each. Where one seam coerces and another validates, the same reported value is allowed through one and denied at the other, and the coercing seam is the dangerous one: a truncated fractional fee debits less than the transaction spends, silently and on every action.
  • Raw-script bypass. Allowlisting by address alone is bypassable with a semantically-equivalent or unrelated raw script; canonicalizing to p2pkh: hash / script: SHA-256 closes this. Implementers MUST NOT allowlist by address string.
  • Budget re-open. A local counter is not authoritative; the chain-seeded, conservative spent_total prevents a wiped/edited counter from re-opening the budget. The seed only ever under-credits remaining budget.
  • Issuer-key pinning. A policy is only as trustworthy as the pin. The engine MUST verify against a build-/config-pinned issuer key it controls, not a key carried in the record or supplied by the agent. Otherwise an attacker who can inject a policy grants themselves authority. Pin rotation is an attended operator action.
  • Token handling. Bearer tokens are secrets: constant-time resolution defeats timing oracles; tokens MUST NOT be logged or derived from the request; and revocation MUST be instant. Because the token is the grant, its leakage is equivalent to granting the agent's authority, but still bounded by the policy and the account balance.
  • Circuit breaker as a backstop, not a boundary. Burst / new-destination breakers latch a freeze on anomalies pending operator review, rather than draining silently. They are a safety net; the hard boundaries remain the caps and the allowlist.
  • Audit fail-loud. Audit must never corrupt fund state or turn success into a retry-inducing exception. Fail-loud but non-fatal isolation of the audit sink is required.
  • Attended fallback vs reject-fast. ESCALATE reintroduces a human and is appropriate for non-HFT agents; the reject-fast default avoids an attended stall on the hot path. Operators choosing ESCALATE should ensure the attended path cannot be starved or bypassed.

References

Was this helpful?

Search Beersy

Search standards by number, title, author or topic