AgentVeins

API reference

Every export of @agentveins/core and @agentveins/adapter-solana.

createGuard

const guard = await createGuard(options: GuardOptions): Promise<Guard>

Async, because it replays the audit log at startup to reconstruct spend and frozen state. Invalid configuration throws here: that is the only place AgentVeins throws for policy reasons.

GuardOptions

OptionTypeNotes
policyPolicyvalidated at construction; bad shapes throw
adaptersWalletAdapter[]at least one required
auditAuditSinkfileAuditSink or memoryAuditSink
agentstringrecorded on every entry
logIdstringnames this agent's log; a log claiming another id is refused
signingKeyKeyObjected25519 private key; signs entries and the anchor
verifyingKeyKeyObjectoptional; derived from signingKey when omitted
anchorAnchorStoreoptional but strongly recommended, without it a deleted log looks like a fresh start
approvalsApprovalStorerequired when policy.approvals is set; createGuard throws without it
requirePersistedStatebooleanthrow if the sink cannot replay
now() => Dateinjectable clock, for tests

Guard

interface Guard {
  pay(req: PayRequest): Promise<PayResult>;
  freeze(): Promise<void>;
  unfreeze(): Promise<void>;
  state(): SpendState;
  flush(): Promise<void>;
}

freeze() closes the kill switch in memory before it returns, so every later pay is already blocked even while a payment waits on a slow rail. Only its audit entry is queued behind that payment: flush() waits for that, and you should call it before exiting a process that just froze.

Payments themselves stay strictly serialized, so two concurrent calls can never race the same budget. unfreeze() is queued in full: the switch may snap shut out of turn, never open out of turn.

Policy

Policy is data: JSON-serializable, versionable, diffable.

interface Policy {
  budgets: Budget[];
  vendors: VendorPolicy;
  recipients?: RecipientPolicy;
  approvals?: ApprovalPolicy;
  velocity?: VelocityRule[];
  killSwitch: KillSwitch;
}

validatePolicy() rejects unknown periods, duplicate periods, unparseable or negative limits, over-precise amounts, an empty allowlist, a non-boolean kill switch, and a velocity rule with no cap or a window over 24h: all at construction time.

Approvals

A payment strictly greater than the threshold is held until a human authorises those exact terms.

approvals?: { above: string };   // "5.00"; a payment of exactly 5.00 passes

pay() does not wait. It returns blocked with approval_required and never reaches the rail, so PayResult keeps its three states and one held payment cannot stall the others — payments are serialized, and anything that waited inside pay() would hold that queue for the length of a human's attention.

interface ApprovalKey   { agent: string; vendorNormalized: string; amountMinor: bigint }
interface Approval      extends ApprovalKey { id: string; expiresAt: string; usedAt: string | null }

interface ApprovalStore {
  grant(input: ApprovalKey & { expiresAt: string }): Promise<Approval>;
  find(key: ApprovalKey): Promise<Approval | null>;
  consume(id: string): Promise<void>;   // atomic; throws if already spent
}

memoryApprovalStore ships from the main entry, fileApprovalStore from @agentveins/core/fs.

An approval authorises one agent, one vendor, one exact amount, once, until it expires. Two grants on the same terms are two authorisations, not one reusable one. It is consumed before the rail is called, so a rail failure burns it and the human is asked again — the same direction the guard already rounds for a settlement it cannot confirm.

What AgentVeins does not do

It never asks anyone. The guard records that a payment needs a person and enforces the answer; routing the request belongs wherever your organisation already makes decisions. Everything needed is in the denial — pay() returns the violation, and the same attempt lands in the audit log with the agent, vendor, amount and reason, so the log doubles as the queue of what is waiting. @agentveins/cli is a worked example of that routing.

SituationResult
No approval, or expired, or already spentblocked / approval_required
The store cannot be read, or the approval cannot be claimedblocked / approval_unavailable. No latch — payments below the threshold keep settling
policy.approvals set with no storethrows at createGuard
Frozen while the gate was waiting on the storeblocked / kill_switch; an approval already spent stays spent

Velocity

The daily budget bounds the damage. Velocity bounds the pace, so a person has time to notice before the budget is the thing that saved them.

velocity?: VelocityRule[];

interface VelocityRule {
  window: string;        // "30s", "10m", "2h" — capped at "24h"
  maxPayments?: number;  // more than this many in the window is refused
  maxAmount?: string;    // more than this much in the window is refused
}

An array, because "at most 20 payments per 10 minutes and at most 5.00 per hour" is one policy rather than a choice. Every rule is checked; the first exceeded returns velocity_exceeded, and the message names the window and the cap.

Caps are inclusive and the candidate payment counts, exactly as the budget behaves: maxPayments: 10 permits the tenth and refuses the eleventh, and maxAmount: "5.00" with 4.99 already in the window refuses a 0.02 payment.

Only money that moved counts. Refusals never extend a window. Counting them would let the guard's own blocks trip further blocks — a spiral where every retry pushes recovery further away — so a hammering agent stays refused for the window's length and no longer.

The window is rebuilt from the log, like the budget: the guard replays the settled and uncertain payments of the last 24 hours at startup, so restarting an agent cannot reset its velocity any more than it can reset its spend.

The one refusal that clears on its own

Every other block needs something to change — a smaller amount, a different vendor, a person's approval, an operator lifting the switch. A velocity block needs only time. That makes it the one case where an agent should wait and retry unchanged, and the MCP guidance and the plugin skill both say so rather than leaving the agent to guess.

Velocity runs after the budget, which is deliberate: a payment over the daily limit reports budget_exceeded, the permanent refusal, rather than the one that reads as "wait a moment".

A window longer than 24h is refused at construction. A velocity rule measured in days is a budget wearing a costume, and the retained window is bounded at 24 hours, so a rule the state cannot answer must not validate.

Results

type ViolationCode =
  | "kill_switch"
  | "vendor_not_allowed"
  | "budget_exceeded"
  | "velocity_exceeded"
  | "approval_required"
  | "approval_unavailable"
  | "invalid_request"
  | "audit_unavailable";

type PaymentErrorCode =
  | "adapter_error"
  | "price_mismatch"
  | "recipient_not_allowed"
  | "insufficient_funds"
  | "timeout";

price_mismatch and recipient_not_allowed mean the vendor asked for something you did not approve: retrying hits the same refusal.

Audit

verifyAuditLog(entries, publicKey, options?): Promise<VerifyResult>
sealAnchor(input, privateKey): Anchor
verifyAnchor(anchor, publicKey): boolean
fileAuditSink(path) · memoryAuditSink(seed?)
fileAnchorStore(path) · memoryAnchorStore(seed?)

verifyAuditLog takes optional logId and anchor: supply both to detect substitution and truncation, not just edits.

Money

parseAmount("25.00"): bigint    // → 25_000_000n
formatAmount(25_000_000n): string
USDC_DECIMALS: 6

solanaAdapter

solanaAdapter({
  keypair,                 // webcrypto.CryptoKeyPair: devnet only
  rpcUrl,
  mode: "direct" | "x402",
  usdcMint?,               // defaults to the devnet USDC mint
  confirmTimeoutMs?,       // default 90_000
  confirmMaxAttempts?,     // default 300
  timeoutMs?,              // per-fetch timeout in x402 mode
})

direct builds an SPL USDC transfer, signs it, submits it, then polls getSignatureStatuses bounded by the transaction's lastValidBlockHeight and by the wall clock and attempt ceilings above.

x402 requests the resource, receives a 402 quote, and attaches a signed transaction in an X-PAYMENT header: built the way the exact SVM scheme requires, with the facilitator as fee payer and only partial signing.

x402 status

x402 mode settles real USDC on devnet. The facilitator is x402's own reference implementation run in process, not a hosted third party, so settlement against someone else's facilitator remains untested. The agent signs a transfer it cannot broadcast and the facilitator pays the fee and submits it: on a settled transaction the agent's SOL balance is unchanged, which is the fee-payer split the scheme depends on.

Devnet only. No mainnet configuration exists in either package.

On this page