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

CommandWhat it does
npm run serverStart 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 mcpStart the MCP stdio server exposing the paid catalog
npm run proxyEnforcement 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 verify267 end-to-end assertions, headless
npm run siteServe 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.

Note: child private keys are stored in gitignored 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)

EndpointPurpose
GET /api/catalogPriced tool catalog
GET /api/ledger · POST /api/ledger/eventFull ledger; client-side event logging
GET /api/stats · GET /api/analyticsPaid/blocked totals; per-tool, per-wallet + per-chain rollups
GET /metricsSame numbers as /api/stats, in Prometheus text-exposition format
GET /api/chains7-chain registry: status, canonical USDC contract, RPC
GET /api/railsRail 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/verifyReceipt lookup + independent ECDSA verification
GET /api/ledger/verify-chainTamper-evidence: confirm the ledger hash-chain is intact; reports the first broken row
GET /api/shadow · PUT · DELETEShadow mode: set a candidate policy that runs without enforcing, read the divergence report, or clear it
POST /proxy/llmAPI-spend rail: estimate → govern → execute → meter actual cost (:8404)
POST /api/catalog/toolsMarketplace: register a paid tool behind the governance gate
POST /api/balances/topupFund a per-chain balance (simulate mode only)
POST /proxy/agentsAgent identities: bearer tokens, optionally wallet-bound (:8404)
GET /api/report?days=NSpend 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.csvAudit export
GET/POST /api/approvals · POST /api/approvals/:id/decideHuman-in-the-loop queue
GET/POST /api/delegations · POST /api/delegations/:id/revokeBudget grants (caps + scopes)
GET/POST /api/freezes · POST /api/freezes/:id/unfreezeKill switch
GET /api/trust/:addressAgent trust score (0–100 + grade) from governance history
POST /api/acp/checkoutACP: 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/verifyRequest 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/verifyDispute 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/spansGovernance decisions as OTLP spans; pass a W3C traceparent and the refusal lands inside the agent trace that caused it
GET /api/policyActive 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.