Jackson Kojo Osei
statusoperationalaccra--:--:--regiongh · uk
pilot

Spend Guard

Spending limits and human approval for AI agents that move money on mobile money.

What it does

AI agents never hold payment keys. They ask Spend Guard to pay, and each request is checked against that agent's policy: allowed and paid out through MoMo One-API, held until a person approves it from a link on their phone, or denied with the exact rule that stopped it. It comes with an MCP server, so Claude and other agents can use it directly. It never holds money.

The problem

When an AI agent holds a payment key, one prompt injection, bug or retry loop can send any amount to anyone, with no record of why. Teams either build a permission layer by hand or don't build one at all.

What you get

Spend Guard approval page on a phone: GHS 350.00, why it was held, spending so far today, the agent's unchecked note, and Approve and Reject buttons
What an approver sees from their link. The agent's own note is marked as unchecked, because that's where an injected instruction would try to talk them into approving.

How to use it

Run Spend Guard, add your payees, give each agent a key and a policy, then point the agent at the MCP server or the HTTP API.

1. Run it

git clone https://github.com/Jacksonoseikojo1122/spend-guard.git
cd spend-guard && npm install
OWNER_KEYS=a-long-random-owner-key npm start   # http://localhost:8790

2. Add a payee and an agent with 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"}'   # returns an sg_agent_ key once
curl -X PUT localhost:8790/v1/agents/$AGENT_ID/policy -H "authorization: Bearer $OWNER_KEY" \
  -H "content-type: application/json" -d @examples/policy.json

3. Let Claude use it (Claude Code)

claude mcp add spend-guard -e SPEND_GUARD_URL=http://localhost:8790 -e SPEND_GUARD_AGENT_KEY=sg_agent_... -- npx tsx /path/to/spend-guard/src/mcp-stdio.ts

4. Turn on payouts

MOMO_URL=https://your-momo-one-api MOMO_API_KEY=... MOMO_WEBHOOK_SECRET=... npm start

Documentation

  • Design Policies, decision order, data model, approvals, payouts, security
  • AI agents (MCP) The three tools and setup for Claude Desktop and Claude Code
  • OpenAPI spec Every endpoint, checked against the app in its tests
  • Example policy Limits, payees, approval and working hours

Features

  • Limits per agent: per payment, per day, per month and per hour, in the agent's own time zone
  • A payee directory only the owner can change, so an agent can't invent where money goes
  • Single-use approval links; opening one never approves, because chat apps preview links
  • Holds that expire, and revoking an agent cancels its pending holds
  • Payouts through MoMo One-API that can't happen twice, with webhook results checked before they're believed
  • An audit log where every entry hashes the one before, so an edited entry shows
  • An MCP server whose answers tell the model not to retry, split a payment or switch recipient

What building it covers

  • AI agent safety
  • Authorisation and policy engines
  • Concurrency and locking
  • Tamper-evident audit logs
  • MCP servers

Stack

  • TypeScript
  • Node.js
  • Hono
  • Zod
  • PostgreSQL
  • SQLite
  • MCP
  • Vitest

Questions

Does Spend Guard hold money?

No. It decides and records. Payouts go through MoMo One-API from your own account at a licensed provider such as Paystack or MTN.

Can an agent raise its own limits or approve its own payment?

No. Agents have their own keys, which can only ask to pay and read their own data. Policies, payees, approvals and revoking need the owner key.

What stops a prompt injection from talking the agent into paying?

The decision only reads typed fields: amount, currency, recipient and the agent's policy. The agent's free-text reason is shown to the human approver, clearly marked as unchecked, but the decision never reads it. And the agent can only pay payees the owner has put in the directory.

What if two payments arrive at the same moment?

Each decision takes a database lock, so the limit check and the save happen as one step. A test sends 20 payments at once against a limit with room for 6, and exactly 6 go through, on SQLite and on Postgres.

Can it pay twice?

No. Every request needs an idempotency key, and the payout to MoMo One-API uses the request's id as its own key, so a retry or a crash returns the same payout instead of making a second one.

Is it ready for real money?

It's a pilot. It has run end to end against MoMo One-API's sandbox, including a failed payout and an approved one, but not yet against a real provider.

Is it free?

Yes, it's open source under the MIT licence.

All projects