MCP server
Governed payments as a tool any MCP-capable agent can call.
An agent that can pay needs the guard between it and the money. Wiring that into the agent means changing the agent. @agentveins/mcp moves it out: the agent calls a tool, the server decides, and the wallet key never enters the agent's process.
That last part is the point. An agent cannot bypass a guard whose key it does not hold.
The short way, on Claude Code
A plugin bundles the server with a skill that teaches an agent how to use it — when to check before planning, how to write a reason worth reading, and which refusals mean stop rather than try something smaller.
/plugin marketplace add AgentVeins/agentveins
/plugin install agentveinsThen point it at a policy by setting AGENTVEINS_POLICY in your environment to that file's
absolute path. Write the policy first — it is the one thing below that has no default.
If the tools do not appear after installing, the plugin's bundled config did not pick up the variable. Add the server directly instead, using the Claude Code block further down; the result is identical.
Everywhere else, and for Claude Code if you would rather be explicit, configure the server yourself.
Setup
One file and two variables. Nothing is installed globally — the MCP client launches the server itself.
mkdir agentveins && cd agentveins
npm init -y && npm install @agentveins/mcpThe policy — policy.json. The rules are the product, so this is the one thing with no default:
{
"budgets": [
{ "period": "per_tx", "limit": "1.00", "currency": "USDC" },
{ "period": "daily", "limit": "10.00", "currency": "USDC" }
],
"vendors": { "mode": "allowlist", "entries": ["api.weather.com"] },
"killSwitch": { "frozen": false }
}That is the whole setup. The audit log, its anchor, the approval store and the signing key are all created beside the policy file on first run.
Why they live beside the policy
The obvious default is the working directory, and it is the wrong one: an MCP client launches this server as a child process with a cwd you neither pick nor see. A missing audit log reads as a first run rather than an error, so a log that landed there would hand back the whole daily budget on every launch from somewhere new. The policy file is a location you chose.
The signing key is created once and reused, never regenerated — a guard replays its audit log at startup and refuses one it cannot verify, so a key that changed between launches would work exactly once. A key file that exists but cannot be read is an error rather than a reason to write a new one, because replacing it would orphan every entry the old key signed.
Wiring it to your agent
The server speaks stdio, which every MCP client launches the same way — a command, its arguments, and an environment. Only the file the config lives in differs.
Claude Code
.mcp.json in your project root:
{
"mcpServers": {
"agentveins": {
"command": "npx",
"args": ["-y", "@agentveins/mcp"],
"env": {
"AGENTVEINS_POLICY": "/abs/path/policy.json",
"AGENTVEINS_RAIL": "mock"
}
}
}
}Or add it from the command line, with one -e per variable:
claude mcp add agentveins \
-e AGENTVEINS_POLICY=/abs/path/policy.json \
-e AGENTVEINS_RAIL=mock \
-- npx -y @agentveins/mcpCodex
~/.codex/config.toml:
[mcp_servers.agentveins]
command = "npx"
args = ["-y", "@agentveins/mcp"]
env = { AGENTVEINS_POLICY = "/abs/path/policy.json", AGENTVEINS_RAIL = "mock" }Claude Desktop
claude_desktop_config.json — on macOS, ~/Library/Application Support/Claude/. Same shape as Claude Code's .mcp.json above.
Anything else
Cursor, Windsurf, Zed and the rest take the same three fields under whatever key they call their server list. If your client can launch a command with an environment, it can run this.
These config files move as clients change, so check your client's current documentation for the path if one above does not match what you see. The server is identical either way — it is the same command, arguments and environment however your client spells them.
Use absolute paths
Give AGENTVEINS_POLICY an absolute path. The client launches the server as a child process with a working directory you did not choose, so a relative one resolves somewhere you did not intend — and since everything else is placed beside the policy, that one path decides where the audit log, the anchor, the approval store and the key all end up.
The tools
pay(to, amount, currency, reason) → settled | blocked | failed check(to, amount, currency, reason) → allowed | blocked, moving nothing spend_state() → every budget, and the kill switch
check lets an agent plan instead of discovering limits by hitting them. It moves no money, writes no audit entry, consumes no budget and spends no approval — and its answer is a snapshot, not a promise: a payment made in between can take the budget.
There is no grant and no unfreeze. An agent may spend what it was allowed and ask what it is allowed, and may not widen either. A tool surface that can lift its own limits is not a governor.
A refusal is a result, not an error
A blocked payment comes back as a normal tool result. Only a rail failure is an error.
This matters more than it looks. If a denial arrived as an error, an agent would read it as a malfunction and retry — walking your allowlist downward, trying smaller amounts, hunting for something that works. The result text tells it what actually happened and what to do: try a cheaper vendor when that would help, and stop and involve a person when it would not.
The plugin's skill states the same thing once, up front, rather than paying for it on every call, and it separates the refusals by what they actually cost. Two stop everything: a frozen guard and a latched one refuse every payment, so walking down the vendor list only lengthens the audit log. One stops a single payment: approval_required parks that payment until a person decides, while other work continues. And one is only a delay: a velocity block clears as the window slides, so the right move is to wait and retry unchanged.
Running against real money
Set the rail to solana and give it a funded devnet keypair:
"AGENTVEINS_RAIL": "solana",
"SOLANA_KEYPAIR_PATH": "/abs/path/devnet-keypair.json",
"SOLANA_RPC_URL": "https://api.devnet.solana.com",
"SOLANA_MODE": "x402"SOLANA_MODE is direct or x402, defaulting to direct. Devnet only — no mainnet configuration exists in this project.
On the mock rail nothing moves and every signature is shaped mock-1-100000, which no explorer resolves. The policy still runs in full, so the governance you see is real even when the settlement is not.
Human approval
Add "approvals": { "above": "5.00" } to the policy and payments above it are held for a person. The store is created beside the policy; there is nothing else to configure. The agent gets blocked with approval_required and is told not to retry until someone has decided. You approve with the CLI, and the agent's next attempt on those exact terms settles, once.
Pace limits
Add velocity rules and a runaway loop is stopped by pace rather than by the daily budget:
"velocity": [
{ "window": "10m", "maxPayments": 20 },
{ "window": "1h", "maxAmount": "5.00" }
]The agent gets blocked with velocity_exceeded and is told to wait rather than to try a
cheaper vendor — the only refusal that clears without anyone doing anything. The window is
rebuilt from the audit log, so restarting the server does not reset it. Full semantics are in
the API reference.
Configuration
| Variable | |
|---|---|
AGENTVEINS_POLICY | Path to the policy JSON. Required |
AGENTVEINS_RAIL | solana or mock. Required, unless SOLANA_KEYPAIR_PATH is set, which implies solana |
AGENTVEINS_SIGNING_KEY | Defaults to operator.key.pem beside the policy, created on first run |
AGENTVEINS_AUDIT | Defaults to audit.jsonl beside the policy |
AGENTVEINS_ANCHOR | Defaults to audit.anchor.json beside the policy. On by default |
AGENTVEINS_APPROVALS | Defaults to approvals.json beside the policy. Used when the policy sets a threshold |
AGENTVEINS_AGENT / _LOG_ID | Identity recorded on every entry |
SOLANA_KEYPAIR_PATH / SOLANA_RPC_URL / SOLANA_MODE | When the rail is solana |
The rail is never inferred as mock — a server reporting settlements while moving nothing is the one guess this must not make. The server refuses to start when a required variable is missing, and names it. Diagnostics go to stderr, always — stdout carries the MCP protocol, and a stray byte there corrupts the session.
Mounting a guard you already have
The binary is a default, not the only path. If you have built a guard — your own adapter, a database-backed approval store, key management you do not want in an environment variable — serve that one:
import { serveGuard } from "@agentveins/mcp";
await serveGuard(myGuard, "solana");serveGuard reads no environment and no files.
What this does not do
It does not ask anyone for approval, or notify anyone that a payment is waiting. It records the decision and enforces it; routing the question to a person belongs wherever your organisation already makes decisions. Everything needed is in the denial — the tool result carries the violation, and the same attempt is in the audit log with the agent, vendor, amount and reason.
One limit worth knowing before you run several agents: fileApprovalStore serialises within one process, not across them, so two servers sharing one approval file can both spend one approval. One process per policy, or a store backed by a database that locks.