Documentation
SpendVeto is a governance gate in front of a catalog of paid (x402/USDC) endpoints, reachable three ways — CLI, delegated child agents, and any MCP client. Everything on this page is exercised by the 267-assertion suite in npm run verify.
Quickstart
$ git clone <repo> && cd spendveto && npm install $ npm run server # API + dashboard on http://localhost:8402 $ npm run call # pays for "review" ($0.01), auto-approved $ npm run call -- summarize # $0.02 — pauses for approval on the dashboard
No keys, no faucets: the default simulate mode uses real secp256k1 keypairs and real ECDSA verification, settling against a local ledger seeded with $5.00 of simulated USDC.
Commands
| Command | What it does |
|---|---|
npm run server | Start the governed API + dashboard (:8402) |
npm run call -- <tool> [--child[=label]] [--chain=id] [--dry-run] | Governed paid call (or a zero-side-effect dry run of the verdict) |
npm run delegate -- <cap> [label] [--parent <label>] [--tools a,b] [--chains id1,id2] [--ttl 10m] | Grant a capped budget — optionally tool-scoped, chain-scoped, and time-boxed |
npm run mcp | Start the MCP stdio server exposing the paid catalog |
npm run proxy | Enforcement proxy (:8404): keyless agents POST intents; custody signs after governance |
npm run policy [-- apply <pack>] | List or apply policy packs (cautious / standard / production) |
npm run apikey -- [role] [label] | Mint an admin-surface API key (role: viewer / approver / admin). First key flips the server to auth-required. |
npm run verify | 267 end-to-end assertions, headless |
npm run site | Serve this marketing site (:8403) |
Governance model
Every call runs the same pipeline, before any payment happens:
- Frozen? — manual kill switch or the runaway-burst detector.
- Policy — per-call cap, hourly budget, call-rate limit, checked against the live ledger.
- Delegation chain — every ancestor's cap and tool scope binds the whole subtree beneath it.
- Human approval — prices above the threshold pause for Approve/Deny; no decision in 30s fails closed.
- Pay + prove — x402 settles; the response carries an ECDSA-signed receipt.
policy.json
{
"maxPerCallUSD": 0.05,
"maxPerHourUSD": 0.2,
"maxCallsPerHour": 10,
"requireApprovalAboveUSD": 0.015,
"anomaly": { "burstAttempts": 10, "burstWindowSeconds": 10 },
"allowedChains": ["base-sepolia", "base"],
"allowedPayees": ["0xVendorA…", "0xVendorB…"], // optional
"alertWebhookUrl": "https://hooks.slack.com/…" // optional
}
Lives at data/policy.json. anomaly: a wallet exceeding burstAttempts ledger events inside burstWindowSeconds is frozen automatically. alertWebhookUrl: freezes, blocked calls, and pending approvals POST there in real time. allowedChains (optional): pins which chains agents may settle on — omit it to allow all seven registered chains. allowedPayees (optional): pins which recipient addresses agents may pay at all — a payment to any other address is refused before it's signed, so a compromised or prompt-injected agent can't reach an attacker's wallet.
Delegation & scoping
$ npm run delegate -- 0.015 "team lead" $ npm run delegate -- 0.05 "intern" --parent "team lead" $ npm run delegate -- 0.02 "translator" --tools translate $ npm run call -- review --child=intern # blocked when team lead's cap would be exceeded $ npm run call -- review --child=translator # blocked: outside delegated scope $ npm run delegate -- 0.02 "base only" --chains base-sepolia $ npm run call -- review --child="base only" --chain=polygon # blocked: outside delegated chain scope
Caps and scopes cascade: a grandchild's spend counts against its own cap and every ancestor's; a revoked link (POST /api/delegations/:id/revoke) kills the whole branch.
data/children.json — fine for simulate-mode demo money; use a real secret store before funding anything.Kill switch
Freeze from the dashboard (per-wallet button) or the API. Frozen wallets are blocked in their own policy check and refused at the simulate payment gate with 403 — even with a correctly signed payment. The anomaly detector applies the same freeze automatically mid-burst.
MCP setup
# Claude Code (server must be running) $ claude mcp add spendveto -- node /abs/path/spendveto/mcp/server.js
Claude sees four tools: review, summarize, translate (priced in their descriptions) and free spendveto_status. Every paid call silently runs the full pipeline; blocked calls come back as tool errors naming the gate.
SDK & integrations
Two ways to call the governed proxy from code instead of the CLI — both ship in this repo (sdk/, integrations/), both exercised by npm run verify against the live proxy, not just parsed.
Node SDK
import { SpendVeto, SpendVetoDenialError } from "./sdk/index.js";
const tg = new SpendVeto({ agentToken: "tg_…" }); // optional — omit while no agents are registered
try {
const result = await tg.pay("review");
} catch (err) {
if (err instanceof SpendVetoDenialError) {
console.log(err.code, err.suggestion); // e.g. "delegation_cap", "pick a cheaper tool…"
}
}
.pay(toolId, opts), .dryRun(toolId, opts), .chat(prompt, opts) (governed LLM/API spend), .registerAgent(label, opts), .catalog(). A blocked call always throws SpendVetoDenialError{code, suggestion, stage} instead of silently no-oping.
LangChain
import { createSpendVetoTools } from "./integrations/langchain.js";
const tools = await createSpendVetoTools({ agentToken: "tg_…" });
// [{ name: "spendveto_review", description: "…", func: async () => "…" }, …]
// pass into tool() from "@langchain/core/tools" for a first-class StructuredTool
Dependency-free — no @langchain/core install required to use these, so it doesn't force LangChain on anyone who isn't using it. One tool per live catalog entry, fetched fresh each call so marketplace tools show up automatically.
HTTP API (:8402)
| Endpoint | Purpose |
|---|---|
GET /api/catalog | Priced tool catalog |
GET /api/ledger · POST /api/ledger/event | Full ledger; client-side event logging |
GET /api/stats · GET /api/analytics | Paid/blocked totals; per-tool, per-wallet + per-chain rollups |
GET /metrics | Same numbers as /api/stats, in Prometheus text-exposition format |
GET /api/chains | 7-chain registry: status, canonical USDC contract, RPC |
GET /api/rails | Rail adapter registry — one pay() contract; x402 live, AP2/ACP/MPP/Safe-AllowanceModule slots (see docs/safe-allowance.md in the repo) |
GET /api/receipts/:id · POST /api/receipts/verify | Receipt lookup + independent ECDSA verification |
GET /api/ledger/verify-chain | Tamper-evidence: confirm the ledger hash-chain is intact; reports the first broken row |
GET /api/shadow · PUT · DELETE | Shadow mode: set a candidate policy that runs without enforcing, read the divergence report, or clear it |
POST /proxy/llm | API-spend rail: estimate → govern → execute → meter actual cost (:8404) |
POST /api/catalog/tools | Marketplace: register a paid tool behind the governance gate |
POST /api/balances/topup | Fund a per-chain balance (simulate mode only) |
POST /proxy/agents | Agent identities: bearer tokens, optionally wallet-bound (:8404) |
GET /api/report?days=N | Spend report: headline, category/chain breakdown, top block reasons |
GET /api/approvals/:id/decide?decision=… | One-click approve/deny (the links the webhook alert carries) |
GET /api/export.csv | Audit export |
GET/POST /api/approvals · POST /api/approvals/:id/decide | Human-in-the-loop queue |
GET/POST /api/delegations · POST /api/delegations/:id/revoke | Budget grants (caps + scopes) |
GET/POST /api/freezes · POST /api/freezes/:id/unfreeze | Kill switch |
GET /api/trust/:address | Agent trust score (0–100 + grade) from governance history |
POST /api/acp/checkout | ACP: check a checkout session against the shared payment token that funds it — merchant, ceiling, category, currency, and whether the declared total matches its own line items |
POST /api/integrity/bind · POST /api/integrity/verify | Request integrity: bind an authorization to a canonical digest of the request, then refuse at execution if the payload changed. Single-use, TTL-bounded, agent-scoped |
GET /api/disputes/:entryHash/evidence · POST /api/disputes/verify | Dispute evidence pack: the spend pinned in its hash chain, the policy in force, the approval and the signed consents — one bundle, signed over its own digest |
GET /api/otel/spans | Governance decisions as OTLP spans; pass a W3C traceparent and the refusal lands inside the agent trace that caused it |
GET /api/policy | Active policy |
Modes
simulate (default): real keypairs, real signature verification, nonce replay protection, local settlement — zero setup. testnet: the real x402 v2 packages, facilitator-adaptive: the gate asks its facilitator what it can settle at boot and brings every supported registry chain live (multi-chain 402s, per-chain USDC). The public facilitator settles Base Sepolia — fund one wallet at faucet.circle.com and set SPENDVETO_MODE=testnet; a CDP facilitator key unlocks its mainnet chains with zero code changes.
Verification
npm run verify boots everything headlessly and runs 267 assertions: forged-signature rejection, all three approval outcomes, cascading caps, tool + chain scoping, chain-scoped signatures with per-chain balances, burst auto-freeze, manual freeze/unfreeze, signed-receipt verification, CSV export, per-chain analytics, webhook delivery to a live receiver, and a real MCP stdio JSON-RPC session.