Getting started
Wrap your agent's payment path so every payment is checked before money moves.
An AI agent with a wallet can spend money. AgentVeins sits between the agent and its money: the agent calls guard.pay() instead of paying directly, and the guard decides whether the payment happens. Every attempt, allowed or refused, is appended to a signed, hash-chained log.
The guard holds no funds and moves no money itself. A wallet adapter does that, and the policy engine has no idea what a blockchain is.
Install
npm install @agentveins/core @agentveins/adapter-solana
# optional: a terminal for approving the payments a guard is holding
npm install -g @agentveins/cli@agentveins/core has zero runtime dependencies. The Solana adapter ships separately so a project governing a different rail never installs the Solana and x402 stack it will not call.
Quickstart
import { createGuard, fileAnchorStore, fileAuditSink, type Policy } from "@agentveins/core";
import { solanaAdapter } from "@agentveins/adapter-solana";
const policy: Policy = {
budgets: [
{ period: "daily", limit: "25.00", currency: "USDC" },
{ period: "per_tx", limit: "1.00", currency: "USDC" },
],
vendors: { mode: "allowlist", entries: ["api.weather.com"] },
recipients: { mode: "allowlist", entries: ["9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"] },
velocity: [{ window: "10m", maxPayments: 20 }],
killSwitch: { frozen: false },
};
const guard = await createGuard({
policy,
agent: "research-agent",
logId: "research-agent-main",
adapters: [solanaAdapter({ keypair, rpcUrl, mode: "direct" })],
audit: fileAuditSink("./audit.jsonl"),
anchor: fileAnchorStore("./audit.anchor.json"),
signingKey,
});
const result = await guard.pay({
to: "https://api.weather.com/forecast",
amount: "0.05",
currency: "USDC",
reason: "forecast query",
});That is the whole integration surface. Everything else is configuration an operator sets up once.
What happens on a payment
Checks run in a fixed order, and the first failure stops everything:
| Order | Check | Question |
|---|---|---|
| 1 | Kill switch | Is this agent authorised at all? |
| 2 | Allowlist | Is this vendor approved? |
| 3 | Budget | Is this within the per-transaction and daily limits? |
| 4 | Velocity | Is this normal behavior, or a runaway loop? |
The order is a security property, twice over. A frozen agent paying an unapproved vendor reports kill_switch, not vendor_not_allowed: you learn the most fundamental reason first. And velocity runs after the budget, so a payment over the daily limit reports the permanent refusal rather than the one that means "wait a moment".
A refused payment never reaches an adapter.
The guard returns before any network call, so nothing is built, signed, or broadcast. A test asserts exactly this for every refusal reason.
The three results
pay() never throws on a refusal.
{ status: "settled"; txSig: string; auditId: string }
{ status: "blocked"; violation: Violation; auditId: string }
{ status: "failed"; error: PaymentError; auditId: string }settled: money moved and the transaction is confirmed on chain.blocked: policy said no. Retrying the same payment fails the same way; adapt instead.failed: the rail failed, not the policy. Some of these are worth retrying, some are not.
Keeping blocked and failed apart is deliberate. Collapsing them would leave an agent unable to tell "you are out of budget, stop" from "the network hiccuped, try again".
Where the spend counter lives
There isn't one. On startup the guard replays the audit log and reconstructs both the spend totals and the frozen state from it. There is no separate counter that could drift, and restarting an agent cannot reset its budget, because the budget is the log.
This is also why the log's integrity matters so much. It is not just evidence, it is enforcement.
Money
Limits are decimal strings at the API boundary ("25.00"), parsed exactly once into bigint minor units, and stay bigint everywhere after. USDC has 6 decimals, and amounts up to the u64 maximum survive the round trip byte-exact. Number never touches an amount.
Budget windows are UTC calendar days. Not local time, not a rolling 24 hours — a velocity window is the rolling one, measured back from now. settled and uncertain payments consume budget; blocked and failed do not. An uncertain payment reached the rail and may have landed, so it is counted — a governor that under-counts unproven spend lets an agent send the same money twice.