AgentVeins

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 agentveins

Then 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/mcp

The 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/mcp

Codex

~/.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_POLICYPath to the policy JSON. Required
AGENTVEINS_RAILsolana or mock. Required, unless SOLANA_KEYPAIR_PATH is set, which implies solana
AGENTVEINS_SIGNING_KEYDefaults to operator.key.pem beside the policy, created on first run
AGENTVEINS_AUDITDefaults to audit.jsonl beside the policy
AGENTVEINS_ANCHORDefaults to audit.anchor.json beside the policy. On by default
AGENTVEINS_APPROVALSDefaults to approvals.json beside the policy. Used when the policy sets a threshold
AGENTVEINS_AGENT / _LOG_IDIdentity recorded on every entry
SOLANA_KEYPAIR_PATH / SOLANA_RPC_URL / SOLANA_MODEWhen 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.

On this page