{"slug": "would-you-let-an-ai-agent-move-your-money", "title": "Would You Let an AI Agent Move Your Money?", "summary": "A developer building an LLM-powered support agent with deterministic boundaries has implemented a risk-tiered gating system that prevents AI agents from executing consequential actions without human approval. The system assigns every action a risk tier in code, with unknown actions refused by default and very-high-risk actions queued as propose-only, ensuring that model confidence is never an authorization mechanism.", "body_md": "*What human-in-the-loop costs once it stops being a stub*\n\nPart 4 of an ongoing experiment: building an LLM-powered support agent with deterministic boundaries. The\n\n[companion repo]grows with the series.\n\nThe rules say the customer is owed a €512.64 refund. The agent agrees. The API is one method call away. **Who presses go?**\n\nThat one line of code is where \"AI-assisted\" becomes \"AI has authority.\" An agent can be perfectly capable of deciding that a refund is justified without being allowed to issue the refund. **Deciding and doing are different permissions.** That's the boundary I wanted to make impossible to blur.\n\nI considered two designs:\n\n**Option A:** let the agent execute whatever tool it decides to call, constrained by prompts and instructions.\n\n**Option B:** assign every action a risk tier in code, then make consequential actions wait for a human regardless of how confident the model is.\n\nI chose **B.** Not because I think the model is always wrong. Because I don't want model confidence to be an authorization mechanism.\n\nThe policy is deterministic:\n\nThat last distinction matters. \"Please don't do this\" is a prompt instruction. \"There is no code path that can do this\" is an architectural property.\n\n``` php\nflowchart LR\n    P[\"Agent proposes action\"] --> G{\"RiskPolicy.tierFor()\"}\n    G -- \"LOW\" --> E[\"Proceeds autonomously<br/>audit recorded\"]\n    G -- \"MEDIUM / HIGH\" --> Q[\"Approval queue<br/>+ audit trail\"]\n    Q --> H[\"Human approves or rejects\"]\n    G -- \"VERY_HIGH\" --> M[\"Queued as propose-only<br/>human executes manually\"]\n    classDef step fill:#eef2f6,stroke:#8fa3b8,color:#24313f\n    classDef decision fill:#f7f4ec,stroke:#b3a988,color:#24313f\n    classDef human fill:#ecf2ed,stroke:#93b39d,color:#3d5344\n    class P,E,Q,M step\n    class G decision\n    class H human\n```\n\nThe consequence is uncomfortable but intentional: **A wrong model decision can produce a wrong proposal. It cannot produce a wrong execution.**\n\nThe enforcement point is one method. Before the switch even runs, there's an important rule: **an action with no assigned risk tier is refused.** No default. No \"probably safe\". No fallback to whatever the model requested. The system fails closed.\n\n```\n// dev/tonal/support/application/GatedActionService.java\nreturn switch (RiskPolicy.tierFor(action)) {\n    case LOW -> {\n        audit.record(\"PROCEEDED %s (LOW) — %s\".formatted(action, \n                description));\n        yield new Result(Outcome.PROCEEDED_AUTONOMOUSLY, null,\n                \"Low-risk action executed with sign-off on output\");\n    }\n    case MEDIUM, HIGH -> {\n        PendingApproval proposal = new PendingApproval(\n                UUID.randomUUID().toString(), \n                action, \n                tier,\n                description, \n                OffsetDateTime.now());\n        String id = queue.enqueue(proposal);\n        audit.record(\"QUEUED %s (%s) — %s\".formatted(action, tier, id));\n        yield new Result(Outcome.QUEUED_FOR_APPROVAL, id,\n                \"Awaiting human approval\");\n    }\n    case VERY_HIGH -> { /* enqueued as propose-only, flagged manual */ }\n};\n```\n\nThree properties do most of the security work.\n\nAn unknown action is refused and audited. The system doesn't guess that an unclassified action is safe.\n\nThis isn't a configuration flag. The service physically doesn't know how to execute a `VERY_HIGH`\n\naction. You can read the class and verify that property.\n\nRefusals. Autonomous executions. Approval requests. The important events all leave an audit record. When someone asks six months later, \"What happened to that refund?\", the answer should be a query — not an archaeological expedition through logs.\n\nI also pinned the strongest claim with a test:\n\n```\n@Test\nvoid veryHighRiskActionsAreNeverExecutedByTheSystem() {\n    GatedActionService.Result result =\n            service.propose(ActionType.DELETE_DATA,\n                  \"purge export artifacts\");\n\n    assertThat(result.outcome())\n            .isEqualTo(\n                  GatedActionService.Outcome.QUEUED_FOR_MANUAL_EXECUTION);\n}\n```\n\nIf someone later adds an execution path for `VERY_HIGH`\n\nactions, I want the test suite to complain **before production does.**\n\nThis is the part that's easy to hand-wave away. It's tempting to say \"just put a human in the loop.\"\n\nFine. **Which human?** Where does the approval request live? How long does it stay valid? How do they know it arrived? What happens if nobody responds? Can the same request be approved twice? What exactly did the agent propose? What did the human actually approve? How do you reconstruct the decision six months later?\n\nThose questions turned \"human approval\" from a boolean into infrastructure. I needed a durable queue, reviewer notification, and an append-only audit trail.\n\nA gate nobody can actually open and review isn't safety. **It's just latency.**\n\nThere's a middle ground between approving every transaction and manually executing everything: pre-authorized mandates.\n\nFor example, an account owner could say:\n\nRefund up to €50 per customer, up to €500 per day, and only to the original payment method.\n\nThe agent proposes the refund. The deterministic policy checks the bounds. If it fits, no individual approval is required.\n\nThe human still owns the authority — they've just exercised it as policy rather than one transaction at a time. It's attractive.\n\nI still deferred it. Because a loose mandate can authorize a lot of quiet mistakes before anyone notices. A real implementation would need expiry, review cadence, tight bounds, recipient constraints, and its own audit trail. That's worth building if the approval queue becomes a measured bottleneck. Not because it feels elegant.\n\nA refund the agent could theoretically issue in three seconds might now wait until a human is available at 8 AM.\n\nThe customer gets:\n\n\"We'll process this within one business day.\"\n\nThat's worse UX. **On purpose.** The latency is the price of keeping execution authority outside the model. And that's why I don't put everything behind the same gate.\n\nA support agent can answer a question instantly. It can classify a ticket. It can draft a response. It can probably update some low-risk metadata without waking anyone up.\n\nBut when the action moves money, changes access, deletes data, or otherwise creates consequences that are difficult to undo, the economics change.\n\nThe goal isn't **human approval everywhere.** The goal is **human ownership where the consequences justify it.**\n\nThe same pattern shows up outside support:\n\nWherever a model meets a consequential action, someone has to own the trigger.\n\nI don't want that someone to be the thing that also guesses.\n\n*And if you think I'm being too cautious with that €512.64, good. That's exactly the argument I want to have. Because the tiers shouldn't be based on vibes. They should evolve when the evidence says they should.*", "url": "https://wpnews.pro/news/would-you-let-an-ai-agent-move-your-money", "canonical_source": "https://dev.to/tonal/would-you-let-an-ai-agent-move-your-money-3bgb", "published_at": "2026-08-30 10:56:29+00:00", "updated_at": "2026-08-30 11:23:36.556068+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "developer-tools", "artificial-intelligence"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/would-you-let-an-ai-agent-move-your-money", "markdown": "https://wpnews.pro/news/would-you-let-an-ai-agent-move-your-money.md", "text": "https://wpnews.pro/news/would-you-let-an-ai-agent-move-your-money.txt", "jsonld": "https://wpnews.pro/news/would-you-let-an-ai-agent-move-your-money.jsonld"}}