Agent Wallet MCP
README.md
# Agent Wallet MCP
[](https://github.com/GitLoopDesign/agent-wallet/actions/workflows/tests.yml)
A **prepaid, capped payment method for an AI agent**, exposed over MCP. The agent
never touches your real bank accounts — it gets an isolated float and a server that
lets it *request* charges but never unilaterally move money.
Safety rests on five properties: **isolation, capping, revocability, out-of-band
confirmation, and auditability.**
> **▶️ Try it in your browser** (no signup, mock money): **https://agentwallet.loopdesign.com.au/demo**
> — make a mock agent request charges and watch the real policy engine auto-clear, hold for
> approval, or deny (including a prompt-injected "$5,000 to a scammer" that gets refused).
> Landing page: **https://agentwallet.loopdesign.com.au**
## The one rule that matters
The agent can only call `request_charge`, which creates a **pending intent**. The
**server** (policy engine) decides whether it clears. Charges at/above your threshold
wait for **you** to approve on a channel the agent cannot reach (`cli_approve.py` or
Telegram). A prompt-injected agent can *ask* to send $5,000 to a scammer; the server
just says no.
## Layers of protection
| Layer | Setting |
|---|---|
| Hard float ceiling (blast radius) | `WALLET_FLOAT_CENTS` (default $200) |
| Per-transaction cap | `WALLET_PER_TXN_CAP_CENTS` (default $50) |
| Rolling 24h spend cap | `WALLET_DAILY_CAP_CENTS` (default $100) |
| Rolling 1h transaction count | `WALLET_HOURLY_TXN_LIMIT` (default 5) |
| Auto-approve threshold | `WALLET_AUTO_APPROVE_UNDER_CENTS` (default 0 = confirm everything) |
| Merchant allow / deny lists | `WALLET_MERCHANT_ALLOWLIST` / `_DENYLIST` |
| Out-of-band approval | `cli_approve.py` (agent cannot call it) |
| Audit log | every request + capture in the `audit` table |
## How a charge flows
```mermaid
flowchart TD
A["AI agent<br/>(can only request)"] -->|request_charge| P{Policy engine<br/>caps · velocity · merchant}
P -->|over cap / denylist / no float| D["DENY<br/>agent cannot override"]
P -->|under auto-approve threshold| C["Capture against float"]
P -->|at / above threshold| H["Pending intent<br/>held for a human"]
H -.->|out-of-band, agent can't reach| U["You: Telegram · dashboard · CLI"]
U -->|approve| RC{Re-check at capture}
U -->|deny| D
RC -->|still in policy| C
RC -->|now out of policy| D
C --> L[("Append-only ledger<br/>balance derived from captures")]
D --> L
```
Everything the agent can touch is on the left; the approval channel on the right is
deliberately out of its reach. [Play with this exact flow in your browser.](https://agentwallet.loopdesign.com.au/demo)
## Quick start (no secrets, mock money)
```bash
pip install -r requirements.txt
python test_policy.py # 8/8 — the policy engine
python test_oauth.py # 8/8 — the OAuth 2.1 core (16 tests total)
python server.py # starts the MCP server (stdio)
```
With `WALLET_AUTO_APPROVE_UNDER_CENTS=0`, every charge the agent requests pauses as
`pending_approval` and prints an approval line to the console:
```bash
python cli_approve.py # list what's waiting
python cli_approve.py <intent_id> # approve + capture
python cli_approve.py <intent_id> --deny
```
## Local dashboard
A single-page web UI to watch the balance, approve/deny pending charges, and read
history — dependency-free (stdlib), bound to `127.0.0.1`, and token-protected. Like the
CLI and Telegram, it's an out-of-band approver the agent cannot reach.
```bash
python dashboard.py # prints http://127.0.0.1:8787/?token=... — open it
```
Set `DASHBOARD_TOKEN` to pin a token, or leave it blank to auto-generate one each run.
## Tools the agent sees
- `get_balance()` — float / spent / remaining
- `get_policy()` — the active limits
- `request_charge(amount, merchant, reason)` — creates a pending intent only
- `get_intent(intent_id)` — poll the outcome
- `list_transactions(limit)` — recent history
## Connecting an agent
- **Claude Desktop / MCP clients (stdio):** add this server (command `python`, arg
`server.py`, cwd = this folder) to the client's MCP config.
- **ChatGPT (HTTP + OAuth):** run `server_http.py`, which wraps the same tools in an
HTTP endpoint fronted by a real OAuth 2.1 flow (see below).
### ChatGPT connector (OAuth)
`server_http.py` (Starlette) serves the MCP endpoint at `/mcp` plus a full OAuth 2.1
authorization server (`oauth.py`): Dynamic Client Registration, Authorization Code +
PKCE, refresh tokens, discovery metadata, and bearer verification scoped to
`wallet.read` + `wallet.request_charge`. The `/authorize` step is gated by your
`OWNER_PASSCODE`, so only you can approve a connector.
```bash
pip install -r requirements.txt
export OAUTH_ISSUER=https://your-public-host # ChatGPT requires HTTPS
export OWNER_PASSCODE=some-strong-secret
uvicorn server_http:app --port 8000 # put an HTTPS tunnel in front
```
In ChatGPT, add a custom connector pointing at `<OAUTH_ISSUER>/mcp`; it discovers the
OAuth endpoints, registers itself, and sends you to `/authorize` to sign in with the
passcode. The OAuth core is unit-tested (`test_oauth.py`), but the end-to-end ChatGPT
flow is yours to deploy behind HTTPS and verify. Never expose `cli_approve.py`,
`dashboard.py`, or the Telegram/Stripe processes to the agent.
## Approve from your phone (Telegram)
1. Create a bot with **@BotFather** → `TELEGRAM_BOT_TOKEN`.
2. Message the bot once, open `https://api.telegram.org/bot<token>/getUpdates`, read
your numeric chat id → `TELEGRAM_CHAT_ID`.
3. `WALLET_APPROVER=telegram`, then run the poller alongside the server:
```bash
python telegram_poller.py
```
Now every over-threshold charge pings your phone with **Approve / Deny** buttons; one
tap finalizes it. Only messages from your own `TELEGRAM_CHAT_ID` are honoured, and the
agent has no access to Telegram — that's what makes it safe. You can also send
`/pending`, `/approve <id>`, `/deny <id>`.
## Going live with Stripe (recommended, economical)
Stripe Issuing is ~$0.10 per virtual card with no forced monthly minimum — you fund a
Stripe Issuing balance and spend against it. First confirm Issuing is **enabled on your
Stripe account** (Dashboard → Issuing; it may need activation, and this is where you
verify current availability in your country).
Stripe cards are **pull-based**: the merchant charges the card, so the hard ceiling is
the card's own `spending_controls`, enforced by Stripe independently of this server.
1. **Create the card once** (test mode first). In a Python REPL:
```python
from issuer import StripeIssuer # needs STRIPE_API_KEY set
print(StripeIssuer().provision_card("Agent Wallet", "you@example.com",
per_auth_cap_cents=5000, monthly_cap_cents=20000))
```
Paste the printed id into `STRIPE_CARD_ID`, set `WALLET_ISSUER=stripe`.
2. **Fund the Stripe Issuing balance** with your set float — that's the max you can lose.
3. **Turn on real-time authorizations** and run the webhook so the policy engine gates
each live charge (auto-declines out-of-policy amounts/merchants):
```bash
stripe listen --forward-to localhost:4242/webhook # Stripe CLI, in one terminal
python stripe_webhook.py # in another
```
For this path set `WALLET_AUTO_APPROVE_UNDER_CENTS` to your per-transaction cap so the
card works up to the cap while float/24h/velocity/merchant limits still bind.
4. **Set `WALLET_APPROVER=telegram`** and run the poller so large buys the agent flags via
`request_charge` hit your phone for a tap before it uses the card.
Alternative issuer (**Airwallex**, `WALLET_ISSUER=airwallex`): auth, a Payouts/Transfers
`execute_payment`, and a reconciliation read are implemented, but untested against the
live API and pricier — test in their demo host and verify fields first.
## Kill switch
Freeze/close the card at the issuer, revoke the agent's OAuth token, or just stop
`server.py`. The float is the maximum you can ever lose.
## Files
| File | Role |
|---|---|
| `config.py` | policy config from env |
| `policy.py` | **the safety boundary** — pure decision function |
| `store.py` | SQLite: intents, captures, audit; balance is derived |
| `issuer.py` | payment execution (mock / **Stripe** / Airwallex) |
| `approvals.py` | out-of-band notify (console / Telegram buttons) |
| `wallet.py` | orchestration shared by server + CLI + poller |
| `server.py` | MCP tools the agent can call |
| `cli_approve.py` | your approval channel — **not** an agent tool |
| `telegram_poller.py` | phone approvals via bot buttons — **not** an agent tool |
| `dashboard.py` | local web approval dashboard — **not** an agent tool |
| `stripe_webhook.py` | real-time Stripe authorization gate — **not** an agent tool |
| `oauth.py` | OAuth 2.1 authorization server for the connector |
| `server_http.py` | HTTP MCP transport + OAuth (ChatGPT connector) |
| `test_policy.py` / `test_oauth.py` | tests for the policy engine and OAuth core |
## Not implemented on purpose
OAuth for the MCP endpoint is left to you. The Stripe calls use the official SDK but
should be run in **test mode** first; the Airwallex calls are untested against the live
service. Anything that touches real money or credentials is yours to verify and
authorise.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues