Skip to content
nomunomu0504.dev

BLOG #AI-AGENTS

spendctl: a corporate card and expense policy for AI agents that pay

Hiroki Nomura 4 MIN READ ai-agents payments x402 typescript
On this page

The problem

APIs that charge over HTTP 402 are multiplying, and agents can pay per request. x402 and MPP (Stripe’s Machine Payments Protocol) are the two paths. What got left behind is control on the buyer’s side. The x402 v2 spec lists client-side budget management as out of scope, SDK guardrails run inside the agent’s own process, and wallet vendors govern only the wallet they hold. The official client has been reported paying a server that inflated its price tenfold (x402#3866).

A prompt is not a spending control. Write “be careful with money” and the model will still pay when talked into it.

What I built

spendctl is a local sidecar that an agent’s HTTP goes through. When the seller answers 402, spendctl reads the price, checks the agent’s policy, asks a human if needed, signs once with your wallet, and writes the receipt to a ledger.

The spendctl.dev landing page
spendctl.dev. The ledger on the right keeps one receipt per payment
  • The agent holds no keys. It can be talked into asking. It cannot be talked into paying
  • Non-custodial. Signing happens inside the sidecar you run, with a Coinbase CDP server wallet or your own key. spendctl never holds keys or funds
  • Refusals and holds come back as plain sentences the model can read
spendctl sidecar between an agent and a paid APIAI agentMCP · SDK · fetchholds no keysrequestspendctl sidecaron your machinereads 402 → checks policysigns once · your walletHTTP 402paid APIthe sellerx402 / MPPprice · payTopolicy.yamlbudgets · allowlistapproval thresholdsledger · SQLiteone receipt per paymentno headers, no bodiesreceipta humanapprove / denyadmin tokenpending_approvalfail-closed: an unknown agent, asset or scheme is denied, with a reason the model can read
The sidecar sits between the agent and the paid API. Keys and signatures never leave it

The policy is one YAML file.

agents:
  researcher:
    budget: { perRequest: "0.05", daily: "1.00", monthly: "20.00" }
    onBudgetExceeded: approve        # over budget: ask a human instead of denying
    requireApprovalAbove: "0.50"
    allow: ["api.weather.dev", "*.search.dev/v1/*"]
    payTo:
      "api.weather.dev": ["0x2096…287C"]   # refuse recipient swaps

Unknown agent, unknown asset, unsupported scheme: each is denied with a reason. Everything fails closed.

What it enforces

Each rule is an end-to-end scenario that runs real HTTP servers (mock sellers, a mock facilitator, the sidecar), and every scenario runs twice on every CI run. There are 22 today. Among them:

  • A payment that would exceed a budget is never signed; spending exactly up to the budget is allowed
  • Concurrent requests never overspend, not even by one payment: budget is reserved inside a SQLite BEGIN IMMEDIATE transaction before signing
  • A 402 naming a different recipient for a pinned host is refused; a price that jumps several times over the last one goes to a human
  • A retry with the same Idempotency-Key is not paid twice
  • A signed payment whose settlement failed is never retried silently, and that host is cut off
  • An agent cannot approve its own payment; approvals need the admin token
  • An agent can withdraw a held payment its task no longer needs, and a late approval pays nothing

Connecting an agent

The base is a gateway URL that works from any language: send the request you would have sent to the seller to the sidecar instead, with the real destination and the agent name in headers. The response carries the decision (settled, denied, pending_approval, and so on) and the amount in headers.

Thin clients sit on top: @spendctl/client for TypeScript (wrapFetch, zero dependencies), spendctl-sdk for Python, and an MCP server. In Claude Code it is one line.

claude mcp add spendctl -e SPENDCTL_AGENT=researcher -- npx spendctl mcp

Connect the login-free sandbox at https://mcp.spendctl.dev/mcp from any MCP client and walk through refusals and approvals against mock paid APIs, with no keys and no chain.

Decisions

  • The sidecar is proprietary; the clients and the wire protocol are Apache-2.0 (miraistudio/spendctl-sdk). The protocol is a handful of headers and a small JSON body, so a client in another language can be written from the table
  • The payment path never leaves your network. The control plane under development syncs policies, approvals and ledgers; it never relays a signature
  • The ledger stores no headers or bodies and drops query strings
  • No percentage of spend, ever

Status

v0.7.3. x402 v1 and v2 (exact on EVM) have been verified through a live payment on Base Sepolia. MPP covers parsing, policy and receipts so far, with live payments still to come. Solana, the upto scheme and HTTPS interception in HTTP_PROXY mode are not done. Details at spendctl.dev/docs.

  1. Forecall: lessons from a linter for MCP tool descriptions 4 min
Hiroki Nomura @nomunomu0504 · CTO, MIRAI STUDIO

Flutter, AWS, homelab. A few side projects and open-source tools. For architecture questions or collaboration, reach out via X DM or the About page. About

COMMENTS · GISCUS