spend-guard
Sends approval notifications for held payments to human approvers via WhatsApp, including a single-use link to approve or reject the payment before it expires.
Spend Guard
Spending limits and human approval for AI agents that move money on mobile money.
Status: pilot (v0.1). Built and tested: the policy format, the decision engine, the HTTP API with idempotency, SQLite and Postgres storage, the tamper-evident audit log, approvals, a recipient directory and payouts through MoMo One-API (run end to end against its sandbox, not yet against a real provider), an MCP server for AI agents, an OpenAPI spec and llms.txt. v0.1 is a pilot: use it with a sandbox until you have run it against your own provider. See the design.
AI agents are starting to pay suppliers, refund customers and send mobile money. Usually the agent holds the provider's secret key and can send any amount to anyone. One prompt injection, bug or retry loop, and the money is gone, with no record of why.
Spend Guard sits between your agents and your payment provider. The agent never sees the provider keys. It asks Spend Guard to pay, and Spend Guard checks the request against that agent's policy:
allowed: sent on to the provider through MoMo One-API.
held: a person approves or rejects it by SMS, WhatsApp or the dashboard.
denied: refused, with the exact rule that stopped it.
It never holds money. Funds stay in your own account at Paystack or MTN. Spend Guard only decides and records.
What you get (planned for v0.1)
Keys per agent, with limits attached: per payment, per day, per month and per hour. Revoke one agent without touching anything else.
A recipient directory the agent can't change. The owner sets each payee's name, phone and network. An agent can only name a payee from the directory, so it can't invent where money goes. Each agent also has its own allow list; payees outside it can be held for approval instead of paid.
Payouts through MoMo One-API. Allowed and approved requests are paid out using the request id as the idempotency key, so a retry or crash can't pay twice. Payout results come back by signed webhook, and Spend Guard confirms each one with MoMo One-API before believing it. Failed and reversed payouts free their room in the limits.
Approval above an amount you choose. Each approver gets a single-use link by SMS, WhatsApp or webhook. Opening the link changes nothing (chat apps preview links automatically); only pressing Approve or Reject does. Unanswered holds expire, so an agent can't wait them out, and revoking an agent cancels its pending holds.
Deny by default. No policy means no payment.
The agent's words don't count. Amount, currency and recipient are typed and validated. The agent's "reason" is shown to the approver but never used to decide.
No double payments.
Idempotency-Keyis required. The same key always gets the same answer.An audit log that shows tampering. Every request, decision, approval and result is recorded, with the policy version and the rule that decided it.
An MCP server, so Claude and other agents can call
request_payment,check_paymentandget_my_limitswith their own key. Held and denied answers tell the model why, and not to retry, split the payment or switch recipient. See Using Spend Guard from an AI agent.
Related MCP server: capline-mcp
Quick start
git clone https://github.com/Jacksonoseikojo1122/spend-guard.git
cd spend-guard && npm install
OWNER_KEYS=choose-a-long-random-owner-key npm start # http://localhost:8790, SQLite by defaultAdd a payee to your directory, create an agent (its key is shown once) and give it a policy:
curl -X PUT localhost:8790/v1/recipients/supplier:kofi-hardware -H "authorization: Bearer $OWNER_KEY" \
-H "content-type: application/json" -d '{"name":"Kofi Hardware","phone":"024 123 4567","network":"mtn"}'
curl -X POST localhost:8790/v1/agents -H "authorization: Bearer $OWNER_KEY" \
-H "content-type: application/json" -d '{"name":"procurement-bot"}'
curl -X PUT localhost:8790/v1/agents/$AGENT_ID/policy -H "authorization: Bearer $OWNER_KEY" \
-H "content-type: application/json" -d @examples/policy.jsonSet DATABASE_URL to use Postgres instead of SQLite.
Example
The agent asks to pay:
curl -X POST localhost:8790/v1/payment-requests \
-H "authorization: Bearer sg_agent_..." -H "Idempotency-Key: invoice-88" \
-H "content-type: application/json" \
-d '{"amount":12000,"currency":"GHS","recipient":"supplier:kofi-hardware","reference":"Invoice 88","reason":"Paying for roofing sheets delivered Tuesday"}'Response from a real run (31 Oct 2026), with a policy that holds anything over GH₵ 100:
{
"id": "pr_Lifkkt4fSRESIlOL",
"status": "held",
"amount": 12000,
"currency": "GHS",
"recipient": "supplier:kofi-hardware",
"reference": "Invoice 88",
"reason": "Paying for roofing sheets delivered Tuesday",
"decision": {
"result": "hold",
"rule": "hold_above",
"message": "Payments of this size need a person to approve them.",
"detail": { "amount": 12000, "threshold": 10000 }
},
"policy_version": 1,
"approval": { "expires_at": "2026-10-31T06:17:19.470Z" }
}A GH₵ 800 request from the same agent is refused with "rule": "per_payment_max" and "detail": {"amount": 80000, "limit": 50000}.
Each approver gets a message like this (from a real run, printed by the console notifier):
Spend Guard: procurement-bot wants to pay GHS 350.00 to supplier:kofi-hardware (Invoice 88).
Payments are only automatic during Mon-Sat 07:00-19:00 (Africa/Accra). Approve or reject before 06:24:
http://localhost:8791/approve/BE62...and the link opens this page. The agent's note is shown as its own unchecked words, because that's where an injected instruction would try to talk the approver into something:
Approvals
Setting | What it does |
| The address approvers open links on, e.g. |
| Where approval notices are POSTed, signed as |
Owners can also answer over the API: GET /v1/payment-requests?status=held, then POST /v1/payment-requests/{id}/approve or /reject. Agent keys can't approve anything.
Payouts
Setting | What it does |
| Your MoMo One-API and its key. Without them, decisions are recorded but nothing is paid out. |
| The |
A real run against MoMo One-API's sandbox (31 Oct 2026), with both services on one laptop:
Request | Decision | Payout |
GH₵ 120 to Kofi Hardware, Invoice 88 | allowed |
|
GH₵ 50 to a sandbox wallet with no money | allowed |
|
GH₵ 450 to Kofi Hardware, Invoice 90 | held, then approved by the owner |
|
Every step, from the decision to the approval to each payout result, is in the audit log, which verified cleanly (14 entries).
Docs
The problem, policies, decision order, API, data model, security, tests and build order | |
The three tools, setup for Claude Desktop, Claude Code and your own agent | |
Every endpoint. A test fails if the app and the spec ever disagree. Also served at | |
A summary for AI assistants helping you integrate. Also served at | |
A policy you can copy |
Related projects
MoMo One-API: one API across MTN MoMo, Telecel Cash, AirtelTigo Money and M-Pesa. Spend Guard sends allowed payments through it.
Webhook Relay: verifies, stores and retries the payment webhooks.
Author
Built by Jackson Kojo Osei, CTO in Accra, Ghana. LinkedIn · Project page · All projects
License
MIT
Available Tools
3 toolscheck_paymentCheck a paymentARead-only
Look up a payment request you made earlier: whether it's still held, approved, rejected or expired, and how its payout went.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | The request id from request_payment, e.g. pr_... |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the safety profile (readOnlyHint=true, openWorldHint=false), so the bar is lower, and the description adds real value by enumerating the possible states (held, approved, rejected, expired) and noting payout outcome. With no output schema, this return-state context is the only place an agent learns what comes back.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
One sentence, front-loaded with the lookup action followed by the two things returned (status and payout), with no wasted wording.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a single-parameter read tool with annotations declaring it safe and non-open-world, the description covers what it does and what the result conveys. The only missing piece is how to interpret edge cases (e.g. nonexistent ids), which is minor here.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100% and the single id parameter is fully documented with format example (pr_...), so the schema does the heavy lifting. The description only echoes that the lookup targets a request the caller made earlier, adding no format or lookup semantics beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ('look up a payment request') and scopes it to requests the caller made earlier, which distinguishes it from the create-side sibling request_payment without naming it. It stops short of an explicit contrast, but the purpose is unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by 'a payment request you made earlier' — call this after requesting a payment to see its state. There is no explicit when-to-use/when-not guidance and no named alternative (e.g. request_payment or get_my_limits) to route between.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_my_limitsSee my spending limitsARead-only
Your spending policy and how much of it you've used today and this month: limits, allowed recipients, when payments need approval, and working hours.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The annotations already declare readOnlyHint=true and openWorldHint=false, so the safety profile is covered. The description adds useful scope by listing the policy information and today/month usage that will be returned, but it does not describe return format, timing, authentication needs, or any other behavioral traits beyond that content list.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single well-formed sentence that front-loads the tool's purpose and uses a compact list to enumerate the relevant policy areas. Every part of the sentence contributes information and there is no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given that the tool has no parameters, no output schema, and annotations that establish it as a safe read-only operation, the description is largely complete. It explains what policy areas and usage periods are reported, though it could explicitly connect this tool to the payment-related sibling tools.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool takes zero parameters and the schema is empty, so there is no parameter semantics to clarify. The baseline for zero parameters is 4, and the description does not create confusion about inputs.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly identifies the resource and contents: spending policy, usage today and this month, limits, allowed recipients, approval rules, and working hours. It lacks an explicit verb and does not name the sibling tools, but an agent can still distinguish it from check_payment and request_payment.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no explicit when-to-use guidance, no when-not-to-use guidance, and no named alternatives. The description only implies that an agent might inspect policy limits before making a payment, leaving the routing decision to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
request_paymentRequest a paymentADestructiveIdempotent
Ask to pay a known recipient by mobile money. Spend Guard checks the request against your spending policy and answers allowed (it's paid out), held (a person must approve it first) or denied. Amounts are in minor units: GHS 120.50 is 12050. The recipient must be an id from your allowed list (see get_my_limits). Always pass an idempotency_key tied to the thing being paid, like 'invoice-88', and reuse it if you retry: the same key never pays twice. If the answer is held or denied, don't retry, split the amount or switch recipient; tell the user.
| Name | Required | Description | Default |
|---|---|---|---|
| reason | No | Why you're paying. Shown to a human approver; it doesn't change the decision. | |
| currency | Yes | ||
| recipient | Yes | A recipient id from your allowed list, e.g. supplier:kofi-hardware | |
| reference | Yes | What it's for, shown to the recipient and the approver, e.g. Invoice 88 | |
| amount_minor | Yes | Amount in minor units (pesewas for GHS). GHS 120.50 is 12050. | |
| idempotency_key | Yes | Stable key for this payment, e.g. invoice-88. Reuse it on retry. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Adds real behavior beyond the annotations: a Spend Guard policy layer evaluates the request, 'allowed' means the money is actually paid out, and the same idempotency key never pays twice. That reconciles the destructiveHint and idempotentHint flags into an operational story an agent can act on, rather than leaving them abstract.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Six tight sentences, front-loaded with purpose and decision outcomes, then units, recipient constraint, idempotency, and the don't-circumvent rule. Every sentence carries an operational instruction and none duplicate structured fields verbatim.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a money-moving mutation with no output schema, the description covers the decision outcomes, idempotency contract, and failure policy well. It does not say what a successful response contains (payment id, final status) that an agent would need to hand off to check_payment, which is the main remaining hole.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is already 83%, so the baseline is 3, but the description adds meaning: minor-unit amounts with a concrete conversion example, the requirement that recipient be an allowed-list id sourced via get_my_limits, and the semantic tie between idempotency_key and the real-world obligation being paid. The currency enum's per-recipient applicability is left unaddressed, keeping it below a 5.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Ask to pay a known recipient by mobile money') and immediately describes the policy-decision outcome space (allowed/held/denied). It also points to the sibling that owns the recipient allow-list (get_my_limits), so an agent can distinguish this from get_my_limits and check_payment without opening a schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives strong when/when-not guidance: always pass an idempotency_key keyed to the underlying obligation, reuse it on retry, and on held/denied do not retry, split the amount, or switch recipient — tell the user instead. The gap is that it never states its relationship to the sibling check_payment (e.g., use that to poll status), so the alternative-tool routing is only partly covered.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.1.0- First observed
check_payment - First observed
get_my_limits - First observed
request_payment
TDQS
Scored across 3 tools
Each tool has a clearly distinct purpose: request_payment initiates a payment, check_payment looks up an existing payment's status, and get_my_limits returns policy/limits information. There is no overlap in action or resource, so an agent can easily choose the right tool.
All names use consistent snake_case with a clear verb_noun structure: check_payment, request_payment, get_my_limits. The pattern is predictable and readable.
Three tools are well-scoped for a narrow spend-guard workflow: initiate, inspect, and retrieve limits. Each tool earns its place, and there is no redundant surface.
The core lifecycle is covered: request a payment, check its status, and understand policy limits. Minor gaps exist, such as listing all payments or canceling a held payment, but these are workaroundable with the current surface.
Maintenance
Related MCP Connectors
Spending control for AI agents: approved budgets, a decision before each payment, signed receipts.
AI agent spending controls. Set limits, blocklists, and caps. Approve transactions from your phone.
Give your AI agent a spending limit: approval controls and single-use virtual cards.
Payment infrastructure for AI agents: spending rules, approval flows, single-use virtual cards.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceAgentPay is the authorization layer between an AI agent and real spending. You define the rules — spending caps, allowed merchants, time windows — and every purchase attempt the agent makes is checked against them in real time. Approved transactions go through. Anything outside the mandate is blocked and logged. No more babysitting every agent action. No more runaway charges.MIT

capline-mcpofficial
AlicenseAqualityDmaintenanceEnables AI agents to make payments with strict spending limits enforced by signed mandates, preventing unauthorized fund movement beyond predefined caps and allowlists.410 npmMIT- AlicenseNot gradedqualityDmaintenanceEnables AI agents to make autonomous payments on Solana with on-chain enforced spending limits, preventing unauthorized fund drainage.4 npm1MIT
- FlicenseNot gradedqualityDmaintenanceEnables human-in-the-loop authorization for AI agent transactions, allowing real-time approval or denial of purchases based on configurable spending limits, vendor blocklists, daily caps, and category restrictions.1-