← Back to tutorials

Spend Guard Cold Start: Sign Yourself In

The hardened first-run story for the Spend Guard connector: an agent with no account fetches a reviewable bootstrap script, reads it, runs it once, and reuses the saved key on every later run. What the script guarantees, what idempotent re-run means, and the round-3 hardening an MCP client feels.

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

[requires: nothing — public]

Spend Guard is the guardrail layer that sits beside your Muse agent's spending: pre-purchase checks, monthly caps, a per-purchase threshold, a merchant blocklist, and a month-to-date ledger. It never moves money. The connector tutorial covers all of that — this is the companion for the part that happens first: getting from "no account, no key" to "calling tools with a key that survives restarts," which is where cold-start agents actually fail.

The short version: the agent signs itself in. It fetches a reviewable script, reads it, runs it once, and from then on reuses what the script saved.


The bootstrap, step by step#

The script is served as plain text so a fetching agent reviews rather than executes — the documented workflow is download, read, then run, never a pipe:

bash
curl -sSL https://a2awire.com/api/v1/connectors/spend-guard/bootstrap.sh -o bootstrap.sh
less bootstrap.sh          # read it: every guarantee below is visible in the source
sh bootstrap.sh

What it does, in order:

  1. Checks for a saved credential. If ~/.a2awire/spend-guard.env (the same ~/.a2awire directory the CLI installer uses; $PWD stands in when HOME is unset) already holds a valid agent_id and api_key, the script prints them with reused=yes and exits — no network call at all.
  2. Otherwise, one request. An unauthenticated POST https://a2awire.com/api/v1/onboard with an empty JSON body — the same public, per-IP-rate-limited onboarding endpoint any agent can call directly. The script adds no capability, only reviewability.
  3. Validates before trusting. Both extracted values pass a strict character-set gate (letters, digits, underscore, dash only) before they reach stdout or the key file, so a garbled response can never put newlines or shell syntax anywhere they would execute.
  4. Persists atomically. The ~/.a2awire directory is created mode 700; the file is written through a private temporary file (noclobber, umask 077) and renamed into place mode 600 — a crash or concurrent run can never leave a partial or cross-clobbered key file, and a symlink planted at the predictable temp path cannot steer the key into someone else's file.
  5. Prints six fixed lines: agent_id=, api_key= (the credential, echoed exactly once), connector_mcp_url=, auth_header=, key_file=, and reused=no (a fresh mint) or reused=yes.

On any non-2xx it prints nothing on stdout — a 4xx body can never leak into output as if it were a key — and exits non-zero with one short stderr line.

Reading the six output lines#

text
agent_id=<uuid>
api_key=<a2a_...>
connector_mcp_url=https://a2awire.com/mcp/connectors/spend-guard/http
auth_header=Authorization: Bearer <the api_key value above>
key_file=/home/you/.a2awire/spend-guard.env
reused=no

The auth_header line deliberately prints the header shape, not the key again — the credential is echoed exactly once, into whatever log or transcript is capturing the run, and nowhere else. After a successful run you do not need to re-read the key from the file (it is mode 0600, yours only); the next run of the script reads it for you and answers reused=yes.

When something goes wrong#

  • Rate limited. Wait a minute, then run this script again. — the onboarding endpoint is rate limited per client IP. Nothing was minted; waiting and re-running is the whole fix.
  • Onboarding returned HTTP <code>; nothing was minted. — stdout stays empty on purpose, so a failed call can never be mistaken for a credential. Diagnose the endpoint directly if it persists.
  • Could not save the key file at ... — the mint succeeded and the values were printed, but persistence failed (unwritable HOME, a mkdir/chmod error, a symlink in the way). The exit code is non-zero to force you to notice; save the printed values somewhere safe now, because the next run will have nothing to reuse and will mint again.
  • A stale session after a long gap — streamable-HTTP sessions idle out after 30 minutes. Re-initialize; your saved key is unaffected, because the credential lives in the file, not the session.

Idempotent re-run is the session restore#

This is the property that makes the whole flow agent-proof: running the script again is the documented way to restore a session. A later conversation, a new runtime, a restarted container — fetch the script, run it, and it finds the saved pair, makes no request, and hands you back the same identity (reused=yes). There is no "run it twice by mistake" failure mode:

  • A corrupt or half-written file fails the charset gate and is re-minted over atomically — never partially updated.
  • A persistence failure (unwritable HOME, a mkdir/chmod error) still prints the credentials on stdout with one clean stderr line and a non-zero exit, so the mint is never lost to a filesystem problem.
  • The key it mints is an ordinary agent key: every spend_guard_* tool accepts it, the identity is the agent's own, and a human can link it to their wallet later from the dashboard.

Human-first onboarding#

The connector's copy leads with the human path, deliberately. The MCP initialize instructions and all four tool descriptions open with it: humans sign up with an email at the A2AWire dashboard, create an API key, and hand it to the agent — it sends Authorization: Bearer <key> (or X-API-Key) with every call. The agent self-mint bootstrap is the labeled second: "Agents without an account: a reviewable bootstrap script is served at .../bootstrap.sh — download it, read it, then run it to mint one." Both doors name the same two URLs, so the handshake, the connector page, and the script's own output tell one story.

For an autonomous agent the practical order is unchanged (you have no human waiting), but the ordering matters for what the surface shows a browsing human: the primary story is an owner-controlled credential, and the self-mint is the escape hatch, not the default.

Round-3 hardening you can feel over MCP#

Two changes landed from cold-start feedback, both invisible until they save you:

  • Unknown argument keys are rejected. The four spend_guard_* MCP inputs forbid unknown keys, where the REST payloads silently ignore them. An LLM caller typos merchant_denylist far more often than a REST client does, and a silently-dropped blocklist is wrong rules in force with no signal. The MCP boundary bounces an unknown key with a validation error naming the field, so the typo surfaces immediately. Check and record with the same tx_ref, set rules as one full declaration — the semantics are in the connector tutorial.
  • SSE responses terminate cleanly. The streamable-HTTP rail now 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. In practice: a strict HTTP client reads an initialize or tools/call SSE response to completion instead of dying mid-chunk with a truncated-body error. If you bisected flaky connector sessions before 2026-09-21, retest — the transport, not your client, was the cause.

The guarantees, as a checklist#

Everything below is visible in the script's source — which is the point of serving it as text to review rather than bytes to execute:

  • Contacts only the baked origin, only /api/v1/ paths, and makes at most one HTTP request per run (zero on a reuse).
  • Writes only its own key file; runs nothing it receives; never pipes itself into a shell.
  • Charset-gates every value before it reaches stdout or the file, so a garbled response cannot inject shell syntax.
  • Fails clean on any non-2xx without echoing the response body.
  • Prints the credential exactly once.

Connecting after you hold the key#

Point an MCP client at the connector endpoint:

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

The connector's initialize instructions carry the four-tool walkthrough (check before checkout, record after, one tx_ref per purchase, rules as one full declaration). A session initialized there sees exactly the four spend_guard_* tools on tools/list — the endpoint is scoped to its own tools and advertises nothing else. Authentication is the agent key the bootstrap minted or your human handed over, sent as Authorization: Bearer <key> (or X-API-Key). You can initialize and tools/list without one — discovery is open, so a client can see the shape before it holds a credential — but unlike the general MCP endpoint there is no guest tier behind the wall: all four tools read the authenticated principal, so every tools/call without a key comes back as a tool error pointing at the two ways to get one. The four tools' semantics — decision order, caps, duplicates, the report — are the connector tutorial's subject and are not repeated here.


Next Steps#