Inspiration

You are giving an AI permission to spend your money. That sentence is easy to say and surprisingly hard to make true.

I kept asking AI assistants to do things for me — move money, buy things, run cloud jobs. And every time, the same uncomfortable question came back: what exactly is this agent allowed to spend? Dashboards can show me what already happened. That is not the same as deciding what is allowed to happen. So I built the thing that was missing: a small, verifiable boundary that sits between an AI agent and any action that costs money. You let it decide for you; you did not hand it an unlimited amount of your money.

What it does

SpendLatch is the authorization boundary between an AI agent and a consequential action. The agent watches spending data, proves a saving opportunity with evidence, and proposes an exact action — then stops. Nothing runs until a human approves a signed mandate: this exact action, on this provider, capped at this dollar amount, once, for fifteen minutes. If the request no longer matches the signed proof, it is rejected. Replaying the mandate fails — the nonce is single-use. Asking the agent to approve itself through MCP is refused and logged. A proposal made on the tool surface crosses to the human on a one-time, fifteen-minute handoff code, so the agent can propose and the person can approve without either side sharing a state file. Every approval exports a proof bundle that can be verified offline, without trusting the running system.

The boundary, stated as seven invariants

Seven invariants, each one checkable against the repository:

  1. Approval and execution are different surfaces. The tool surface cannot submit an approval: approve_action carries no session credential, so the call is refused and logged. Signing requires a web session token issued to the web surface. This is a capability boundary, not an identity boundary — the one-time handoff code is returned in the propose_action result, so the calling host is trusted to hand it to a person, and the server cannot verify who redeems it.
  2. No mandate, no motion. An action without a valid mandate is refused, never queued.
  3. A mandate is single-use, and single-use is not bookkeeping: it lives outside the bounded ledger, so log trimming or a status rewind cannot re-arm a spent mandate.
  4. An issued mandate is immutable. Raise the cap after approval and the signature breaks — it becomes an unsigned mandate, not a bigger one.
  5. Approval expires in fifteen minutes, a server constant no caller can set.
  6. The evidence you approved is the evidence that runs. Proof, cap, and adapter-registry drift are each refused separately.
  7. Front-end visibility is never an authorization primitive. Hide or disable any control and the server refuses exactly the same way — and records it either way.

Every one of these has a test that fails when the guard behind it is deleted, and tools/mutation_check.py proves that by deleting them.

How we built it

  • A deterministic policy gate (Python) parses every request into a SpendIntent and decides — the LLM never owns financial authority.
  • A plan-check-execute ledger keeps the hash-chained decision ledger: every alert, hold, suppression and refusal is recorded with evidence.
  • A sandbox AWS adapter runs the approved action and issues a receipt bound to the mandate.
  • An MCP server (2025-11-25, Streamable HTTP) exposes the same tools to agent surfaces, but signing requires a web session token — an approve_action call carrying none is refused and logged.
  • The web demo (FastAPI backend, zero-build vanilla JS frontend) runs live at https://spendlatch.owain32380.cn — you can run the full loop yourself: ask, prove, approve, execute, replay.

Challenges we ran into

The hard part was not the agent — it was making the boundary boring and precise: what a mandate can and cannot express, replay protection under a single-use nonce with a 15-minute expiry, and an AWS adapter labelled honestly ("simulated adapter") rather than dressed up.

Accomplishments that we're proud of

  • The agent proposes but cannot authorize itself — demonstrated live, including a logged refusal.
  • Every ID is chained: execution → mandate → proposal → the evidence the human actually read.
  • The demo is reproducible from open-source code, and the proof bundle verifies offline.

What we learned

Authorization is a product surface, not a middleware detail. The moment you make it visible — mandate cards, receipts, a decision ledger — people start asking for it everywhere.

What we got wrong

The strongest claims in this project came from people trying to break it, so we ran that on ourselves before submitting. Three findings changed the code:

  • The evidence gate could not fail. CI checks out a single commit, so git log -1 -- <path> always returns the tip, and "evidence must not predate the code" was trivially true — green for months, proving nothing. With fetch-depth: 0 the same commit went red: five artifacts were older than the code they describe. They are refreshed now, and the gate demonstrably fails when it should.
  • A gate graded its own homework. The test-count step rewrites an evidence file and ran before the integrity check, so it compared the manifest against a file it had just rewritten. It runs last now.
  • One invariant was true in the code and false in the system. approve_action refuses without a web session — but the one-time handoff code travels back through propose_action to whoever called it, and a session token costs nothing to mint. An agent that speaks both MCP and HTTP can therefore walk the whole loop without a human. We did not paper over it: docs/THREAT-MODEL.md (L1) states it plainly, and the invariant is worded as a capability boundary rather than an identity boundary. Closing it needs an identity provider, which this build does not have.

We would rather hand in a submission that names its own worst case than one that has to be caught.

What's next

Bring SpendLatch to Alexa+: a human approves on the phone, the voice agent can then execute exactly what was approved — once, within the cap. Real AWS API calls through the adapter. Conformance tests for anyone building their own boundary.


Why the Alexa+ track

An agentic voice surface is the case where a spending boundary matters most: a voice agent that can move money has no screen on which a person can read a diff, and "it sounded like it agreed" is not consent. SpendLatch puts the approval where a screen still exists — a signed mandate the person confirms — and lets the voice agent execute that exact mandate once.

Where the integration actually stands: the MCP server and the approval surface are built and live at protocol 2025-11-25 over Streamable HTTP. The voice conversation in the demo is a faithful simulation, because the Alexa+ skill runtime is not exposed to hackathon entrants — there is no sandbox in which a submission could exercise it. So the tool layer, the mandate gate, the ledger and the refusals are real code doing real work; the speech layer in front of them is a stand-in, and it is labelled as one on the page and in the UI. Wiring it to the real skill flow is integration, not invention: both sides already speak MCP.

What this would use on AWS

The provider adapter is simulated today — it names the API it would call, labels its receipt simulated adapter, and refuses to pretend otherwise. A real deployment swaps one module; the gate, ledger and verifier are unchanged.

Why the Open Source mini challenge

The whole boundary — policy gate, hash-chained ledger, provider adapters, offline verifier — is MIT-licensed and reproducible from https://github.com/owain323/spendlatch. New repository, created 2026-09-18, inside the hackathon window; the contribution is the project itself.

  • Repository and contribution URL: https://github.com/owain323/spendlatch · GitHub username: owain323
  • What was published: a deterministic policy gate; a SHA-256 hash-chained ledger that records every decision including every refusal; a mandate (HMAC-SHA256, single-use, scope-capped, 15-minute expiry) as the only thing that unlocks execution; an offline verifier for exported proof bundles; thirteen tools over MCP 2025-11-25 / Streamable HTTP, with the approval card served as a ui:// resource.
  • Not only fixtures: tools/analyze_usage.py reads a real usage export (10,901 requests across 137 days) and applies the same credits-per-call canary the demo uses. Seven of the 23 anonymized model ids carry enough history to judge, and all seven drift. The export stays local; the tool and its output ship.
  • How to check it: python run_checks.py — 239 tests, three benchmark tiers, an MCP roundtrip against a real server subprocess, an integrity manifest over 122 files, evidence freshness, and a mutation check that deletes each of twelve guards. Five minutes, zero credentials.

The friction log

Published in the required format at https://github.com/owain323/spendlatch/blob/main/friction-log.md — task attempted, steps taken, expected versus actual, severity, workaround, suggestion. Two entries that cost the most time:

  • MCP Python SDK 2.0.0 removed mcp.server.fastmcp. An unpinned install boots and dies. Suggestion: keep the import path stable across a major, or ship a deprecation shim.
  • Tool schemas are generated from docstrings, so the safety hints a host relies on drift silently. Our conformance probe found four gaps on its first run. Suggestion: make readOnly/destructive/idempotent first-class fields rather than prose.

Built With

Share this project:

Updates

Submission history