← Back to tutorials

Spending Guardrails for a Muse Agent (Spend Guard)

Connect your Muse agent to A2AWire Spend Guard: pre-purchase checks, monthly caps, a per-purchase approval threshold, a merchant blocklist, and a month-to-date spend ledger. Free, and it never moves money.

Author
A2AWire
Published
Category
Security
Difficulty
beginner
Reading time
12 min read
On this page

[requires: agent key]

Muse can buy things for you: it executes purchases on its own payment methods. Nothing in that loop verifies a spending cap, asks you before an unusually large single purchase, or keeps a ledger of what your agent actually spent. Spend Guard is that missing loop, and it sits beside the money: it advises and records, it never moves money itself.

This tutorial walks the full connector: the three-step setup in Muse, the tool semantics your agent will rely on, REST and MCP examples you can run yourself, and a stdlib-only live validation script. The hardened first-run story - the reviewable bootstrap, key persistence, the round-3 reliability changes - is the companion tutorial: Spend Guard Cold Start.

The public setup page is a2awire.com/connectors/spend-guard/.


The three setup steps#

  1. Paste the setup prompt into Muse. The prompt is reproduced verbatim below and on the Muse connector page.
  2. The agent signs itself in. No human setup required: on first run it fetches the reviewable bootstrap script, reads it, runs it to mint its own API key, and sends it with every call. The script saves the key to ~/.a2awire/spend-guard.env, so a later run restores the session with no new mint. A user who already has an A2AWire key can hand it over instead. (The connector's MCP copy - the initialize instructions and all four tool descriptions - names those two doors in the other order, human first and agent self-mint second, so a client that reads only the handshake still sees the owner-controlled credential. The setup page leads with the agent, as this list does. Either door ends at the same key.)
  3. Talk to Muse naturally. For example: "Cap my agent at $50 a month and check with me over $25." The agent asks for your rules and applies them in one call.

The setup prompt, verbatim:

text
Build a custom integration to the A2AWire Spend Guard service. Its MCP server URL is https://a2awire.com/mcp/connectors/spend-guard/http (remote, streamable HTTP). Sign yourself in without waiting for me: if you do not already hold an A2AWire API key, fetch https://a2awire.com/api/v1/connectors/spend-guard/bootstrap.sh, read it, then run it to mint your own key. The script saves that key to ~/.a2awire/spend-guard.env and reuses it on later runs, so running it again restores your session instead of minting a second key. If I already have an A2AWire API key, I will give it to you instead. Send the key with every call (Authorization: Bearer). Before you buy anything for me, call spend_guard_check_purchase and follow its decision: approve, deny, or ask me. After any purchase, call spend_guard_record_purchase with the same tx_ref you used at check time. Set my rules with spend_guard_set_rules in one call: cap $[X] per month, ask me above $[Y], and no purchases from [merchants]. Ask me for X, Y, and the merchants now. Test every tool end to end and save the integration as a reusable skill.

No A2AWire key yet#

An agent working without a human signs itself in: GET https://a2awire.com/api/v1/connectors/spend-guard/bootstrap.sh serves a short POSIX shell script as plain text - download it, read it, then run it, never pipe it into a shell. The script owns key persistence end to end: it makes exactly one request (the public POST /api/v1/onboard), saves the minted pair to ~/.a2awire/spend-guard.env (mode 0600 under a 0700 directory - the only file it writes), and an idempotent re-run reuses the saved key and makes no request at all, which is the documented way to restore a session in a later conversation. The minted key is an ordinary agent key every spend_guard_* tool accepts, and a human can link the identity to their email wallet later from the dashboard. Prefer using your own account? Create a wallet with your email at https://a2awire.com/wallet/ and hand the agent that key instead.

What the script guarantees line by line - the six fixed output lines, the charset gate, the atomic write, every failure mode - is the cold-start tutorial's subject: Spend Guard Cold Start.


The four tools#

ToolWhen the agent calls itWhat it returns
spend_guard_check_purchaseBEFORE checkoutapprove, deny, or ask_user, with a plain-English reason, the remaining monthly budget when a cap exists, and a check_id
spend_guard_record_purchaseAFTER the purchase completesthe recorded purchase, whether it was an idempotent replay (recorded_previously), and the month-to-date total
spend_guard_set_ruleswhen the user states or changes rulesthe effective rules
spend_guard_reportwhen the user asks about spendmonth-to-date total, purchase count, top merchants (up to 5), current rules, remaining budget

Semantics your agent should know#

  • Decision order (deterministic and explainable): blocklist -> monthly cap -> duplicate detection -> per-purchase threshold -> approve. A deny always beats an ask.
  • Observation mode. With no rules set, every check approves and purchases are still recorded, so the report works from the first purchase.
  • One tx_ref per purchase. Mint an idempotency key when you check, and pass the same value when you record. Uniqueness applies to recorded purchases; checks are exempt, so sharing the key between a check and its record is the documented flow. Replaying a record with the same tx_ref returns the original record and does not double-count.
  • Rules are a full declaration. spend_guard_set_rules replaces the whole rule set in one call: set the cap, the threshold, and the blocklist together. Omitting or nulling a field clears that rule, so never make a second call to "add" one rule - it silently clears the others.
  • Threshold is strictly above. A purchase at exactly the threshold approves; above it asks.
  • Cap is cumulative. The cap compares the month-to-date total of recorded purchases plus the new amount against the cap; checks alone never consume budget.
  • Duplicate detection needs no rule. The same merchant and the same amount recorded in the last 10 minutes comes back ask_user.
  • USD only. Amounts are amount_usd (2 decimal places, greater than zero, at most 100000). Any other currency returns a clear validation error.

REST examples#

Every endpoint takes the agent API key as X-API-Key and scopes everything to the key's owner. Money is sent and returned as 2-decimal-place strings.

Set the rules (one full declaration - cap $50, ask above $25, block one merchant):

bash
curl -X PUT https://a2awire.com/api/v1/spend-guard/rules \
  -H "X-API-Key: $A2AWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "monthly_cap_usd": "50",
        "per_purchase_threshold_usd": "25",
        "merchant_denylist": ["Party Warehouse"]
      }'

Check a proposed purchase before checkout (approve path, with the one tx_ref you will reuse on the record call):

bash
curl -X POST https://a2awire.com/api/v1/spend-guard/check \
  -H "X-API-Key: $A2AWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"merchant": "Balloon Co", "amount_usd": "15.00", "tx_ref": "balloons-1"}'
# -> {"check_id": "...", "decision": "approve",
#     "reason": "Approved: within your spend rules and no duplicate detected.",
#     "remaining_monthly_budget_usd": "50.00", "merchant": "Balloon Co", "amount_usd": "15.00"}
# remaining_monthly_budget_usd is the budget BEFORE this purchase. Checks never
# consume budget, so it only drops once the record call below lands.

Record the completed purchase (same tx_ref, plus the check_id link):

bash
curl -X POST https://a2awire.com/api/v1/spend-guard/record \
  -H "X-API-Key: $A2AWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"merchant": "Balloon Co", "amount_usd": "15.00",
       "tx_ref": "balloons-1", "check_id": "<check_id from above>"}'
# -> {"recorded_previously": false, "month_to_date_total_usd": "15.00", ...}
# recorded_previously is true only when this tx_ref was already in your
# ledger before the call (an idempotent replay); false on first write.

Read the month-to-date report:

bash
curl https://a2awire.com/api/v1/spend-guard/report \
  -H "X-API-Key: $A2AWIRE_API_KEY"

MCP example#

The connector's MCP endpoint is a remote streamable-HTTP server. Point an MCP client at:

text
https://a2awire.com/mcp/connectors/spend-guard/http

A session initialized on that URL sees exactly the four spend_guard_* tools on tools/list - the connector endpoint is scoped to its own tools and advertises nothing else. Authentication is the same agent key, sent as Authorization: Bearer <key> (or X-API-Key). Discovery is open: you can initialize and tools/list without a key to see the shape before you hold a credential, but all four tools read the authenticated principal, so a tools/call without one comes back as a tool error pointing at the two ways to get a key. For A2AWire's general tool catalog, connect to https://a2awire.com/mcp/http instead.

Raw JSON-RPC, minimal:

bash
# initialize (keep the session id from the response headers)
curl -sS https://a2awire.com/mcp/connectors/spend-guard/http \
  -H "Authorization: Bearer $A2AWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18","capabilities":{},
                 "clientInfo":{"name":"spend-guard-example","version":"1.0"}}}'

# tools/call on the same session (Mcp-Session-Id header from initialize)
curl -sS https://a2awire.com/mcp/connectors/spend-guard/http \
  -H "Authorization: Bearer $A2AWIRE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Mcp-Session-Id: $MCP_SESSION_ID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
       "params":{"name":"spend_guard_check_purchase",
                 "arguments":{"merchant":"Balloon Co","amount_usd":"15.00",
                              "tx_ref":"balloons-1"}}}'

For the wider MCP surface (the same identity and key, the general endpoints), see Connecting via MCP.


Troubleshooting#

A dropped stream mid-call. The connector endpoint speaks streamable HTTP, and a proxy or load balancer can cut a long-lived stream before the response is delivered - your client reports an incomplete read or a remote disconnect. Retry the same call: spend_guard_report is read-only; a repeated spend_guard_check_purchase appends a second check to the ledger but never moves budget and never seeds a duplicate, so it returns the same decision; spend_guard_record_purchase is idempotent on tx_ref (a replay returns the original record without double counting); and spend_guard_set_rules is a full overwrite, so the same arguments re-apply the same rules. A dropped stream is a transport-level event; it never double-counts a purchase or half-applies a rule change.

A typo'd argument key. The four spend_guard_* MCP inputs reject unknown keys, where the REST payloads silently ignore them. An LLM caller mistypes merchant_denylist far more often than a REST client does, and a silently-dropped blocklist would mean wrong rules in force with no signal - so the MCP boundary bounces an unknown key with a validation error naming the field, and the typo surfaces immediately. The REST door keeps its lenient behavior; only the MCP boundary is strict.

A strict client that kept dying mid-response. The streamable-HTTP rail pins an MCP SDK version with the upstream fix for dirty POST-SSE termination: each per-request stream is buffered and closed as a complete unit, so a strict HTTP client reads an initialize or tools/call response to completion instead of failing on a truncated body. If you bisected flaky connector sessions before 2026-09-21, retest - the transport, not your client, was the cause.

Both of those are round-3 reliability hardening, documented from the client's seat - along with the human-first ordering of the connector's MCP copy and the full cold-run walkthrough - in Spend Guard Cold Start.


Live validation script (stdlib only)#

The script below drives the whole flow against the live REST endpoints with no dependencies: rules -> check -> record -> replay -> report. It is a condensed form of examples/spend_guard_quickstart/spend_guard_quickstart.py, which additionally walks the deny and ask-me paths. Re-running either script reuses tx_ref "balloons-1", so the record call replays instead of double-counting and the check may come back ask_user inside the 10-minute duplicate window - both are the documented behaviour, not a failure.

bash
A2AWIRE_API_KEY=<your key> python3 spend_guard_quickstart.py https://a2awire.com
python
import json, os, sys, urllib.request, urllib.error

BASE_URL = (sys.argv[1] if len(sys.argv) > 1 else "https://a2awire.com").rstrip("/")
API_KEY = os.environ.get("A2AWIRE_API_KEY", "")

def call(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    request = urllib.request.Request(
        BASE_URL + path, data=data, method=method,
        headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
    )
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            return json.loads(response.read().decode())
    except urllib.error.HTTPError as error:
        raise SystemExit(f"{method} {path} -> HTTP {error.code}: {error.read().decode()}")

# 1. Rules - one full declaration (a second call REPLACES these rules).
call("PUT", "/api/v1/spend-guard/rules", {
    "monthly_cap_usd": "50",
    "per_purchase_threshold_usd": "25",
    "merchant_denylist": ["Party Warehouse"],
})

# 2. Check a cheap purchase (approve path). Mint ONE tx_ref for this purchase.
check = call("POST", "/api/v1/spend-guard/check", {
    "merchant": "Balloon Co", "amount_usd": "15.00", "tx_ref": "balloons-1",
})
print(check["decision"], "-", check["reason"])

# 3. Record the purchase - same tx_ref as its check, plus the check_id link.
record = call("POST", "/api/v1/spend-guard/record", {
    "merchant": "Balloon Co", "amount_usd": "15.00",
    "tx_ref": "balloons-1", "check_id": check["check_id"],
})
print("month to date:", record["month_to_date_total_usd"])

# 4. Replay the same record - idempotent, not double-counted.
replay = call("POST", "/api/v1/spend-guard/record", {
    "merchant": "Balloon Co", "amount_usd": "15.00", "tx_ref": "balloons-1",
})
assert replay["recorded_previously"] is True  # idempotent replay, not double-counted

# 5. Report - month-to-date total, top merchants, remaining budget.
report = call("GET", "/api/v1/spend-guard/report")
print("Spend Guard live validation passed:", json.dumps(report, indent=2))

The party-planning variant (cap $200, threshold $25, one held purchase, report at the end) is the demo script in the connector's submission copy: connectors/001-spend-guard/SUBMISSION.md.


What this is and is not#

Purchases still run on Muse's own payment methods. Spend Guard advises and records what your agent spends: it does not move money, does not hold cards, and does not touch crypto. It stores your rules and spend metadata (merchant, amount, and the references you supply) and nothing else; it never sees a card number. It acts only when your agent calls it - no webhooks, no background access.


Next Steps#