VetoVetoDocs
Guides

Policies & trust

Set the rules that decide which agent spends you accept — dollar caps, velocity limits, mandate requirements, reputation floors, and review routing — then publish them safely as immutable versions.

Your policy is the set of local rules the acceptance gate evaluates on every settle, before any money is captured. It's how you decide which agent purchases to accept, hold for review, or reject — by dollar amount, velocity, authorization (mandate), agent reputation, and intent. This guide is the practical "how do I set it up" recipe; the Policy concept is the field-by-field reference.

Start from a preset

Adopt a posture wholesale, then tune individual fields. The presets are functions (each call returns a fresh, isolated policy), so one merchant can never mutate another's:

The recommended default. Small spends clear on reputation; larger ones need a mandate; generous-but-bounded caps; only the weakest tier goes to review.

import { BALANCED } from '@veto-protocol/checkout';

const policy = BALANCED();
// requireMandateOverUsd: 20, minReputationTier: 'standard', maxPerTransactionUsd: 500,
// maxPerAgentPerDayUsd: 2000, ratePerAgentPerHour: 60, ratePerAgentPerDay: 300,
// holdForReviewBelowTier: 'cautious'

For high-value or sensitive goods. A mandate is required for any spend, reputation must be trusted, caps are modest, velocity is tight, and anything not clearly trusted is sent to human review. "I'd rather lose a sale than eat a chargeback."

import { STRICT } from '@veto-protocol/checkout';

const policy = STRICT();
// requireMandateOverUsd: 0 (mandate for ANY spend), minReputationTier: 'trusted',
// maxPerTransactionUsd: 100, maxPerAgentPerDayUsd: 250,
// ratePerAgentPerHour: 10, ratePerAgentPerDay: 50, holdForReviewBelowTier: 'trusted'

Maximum reach, minimum friction — cheap digital goods and content APIs where the rail itself (paid-up-front x402) is the guarantee and a bad sale costs almost nothing. No mandate requirement, accept any reputation, high cap, no rate limit, never hold.

import { OPEN } from '@veto-protocol/checkout';

const policy = OPEN();
// requireMandateOverUsd: ∞ (never require), minReputationTier: 'risky',
// maxPerTransactionUsd: 10000, no daily cap, no rate limits, never holds
apply and tweak
import { createCheckout, BALANCED } from '@veto-protocol/checkout';

const checkout = createCheckout({
  // …
  policy: { ...BALANCED(), maxPerTransactionUsd: 250, requireMandateOverUsd: 50 },
});

Every field is optional — safe defaults fill the rest. A field you don't set stays unset, and "no cap" is a distinct, legitimate posture from "default cap": leaving maxPerAgentPerDayUsd undefined means no daily cap, not the preset's value.

The dials, and the code each fires

The gate evaluates these in order and returns a verdict with one or more stable reason codes. Set what matches your risk:

DialFieldFires
Per-transaction capmaxPerTransactionUsdOVER_PER_TX_CAP
Daily spend cap (per agent)maxPerAgentPerDayUsdOVER_DAILY_CAP
Hourly rate (per agent)ratePerAgentPerHourRATE_LIMITED_HOURLY
Daily rate (per agent)ratePerAgentPerDayRATE_LIMITED_DAILY
Require a mandate above $XrequireMandateOverUsdMANDATE_REQUIRED
Reputation floorminReputationTierREPUTATION_TOO_LOW
BlocklistblockedAgentsAGENT_BLOCKED
Allowed railsallowedRailsRAIL_NOT_ALLOWED
Forbidden intent wordsforbiddenIntentKeywordsINTENT_FORBIDDEN_KEYWORD
Intent must match cartrequiredIntentMatchINTENT_MISMATCH
Review below a trust tierholdForReviewBelowTierHOLD_FOR_REVIEW

How caps, mandates, and reputation interact

The three trust inputs aren't independent — they combine into one verdict:

Caps and velocity are hard walls

A spend over maxPerTransactionUsd, over the rolling daily cap, or over a rate limit is rejected outright — no mandate or reputation overrides a cap.

Mandates unlock higher-value spends

Above requireMandateOverUsd, the agent must present a valid, bound mandate. A Veto mandate that's signed, approved, unexpired, amount-sufficient, and merchant-matched resolves to the premium tier. No mandate over the threshold → MANDATE_REQUIRED (rejected) or MANDATE_REQUIRED_HOLD (held), depending on your posture.

Reputation is the floor for the rest

For spends that don't need a mandate, the agent's hosted reputation tier must clear minReputationTier. Unknown agents read as standard — never as risky by default — and reputation always degrades safe: if the lookup is slow or down, the gate falls back to the neutral default rather than blocking the sale.

Review routing catches the ambiguous middle

holdForReviewBelowTier sends anything below a trust tier to a 202 hold with a review_url, instead of an outright reject — the right move when you'd rather a human look than lose a borderline sale.

The intent-match footgun.

requiredIntentMatch: true holds every transaction when no intentMatcher is wired (there's no score to compare against). If you want intent matching, set the flag and provide a intentMatcher in your config — otherwise leave it off. This is why the STRICT preset deliberately does not enable it.

Editing policy on the hosted plane

Hosted policies are immutable and versioned — you never mutate an active policy in place. Each change creates a new inactive version; publish flips it active in one transaction. A receipt embeds the policyHash, so every order provably names the exact policy version that accepted it.

Write a new version

curl -X PUT https://api.veto-checkout.com/v1/policy \
  -H "Authorization: Bearer veto_test_…" -H "Content-Type: application/json" \
  -d '{ "body": { "requireMandateOverUsd": 50, "maxPerTransactionUsd": 250, "minReputationTier": "standard" } }'
# → { "version": 2, "hash": "…", "active": false }

Publish the gate

POST /v1/publish runs the SDK's validateConfig() over your draft and refuses to publish a foot-gun config (returns 422 with CONFIG_INVALID + findings). If it passes, it activates the newest version and invalidates the manifest cache so the next discovery reflects it.

curl -X POST https://api.veto-checkout.com/v1/publish -H "Authorization: Bearer veto_test_…"
# → { "published": true, "active_policy_version": 2 }

Prefer the CLI? veto policy show prints the active policy and veto policy set <field> <value> creates a new version in one line.

Validate before you self-host

Self-hosting, run the same shared validator over your config before you ship it — it catches the foot-guns at build time instead of at an agent's first request:

import { validateConfig } from '@veto-protocol/checkout';

const findings = validateConfig(myConfigInput);
if (findings.length) throw new Error(`config invalid: ${JSON.stringify(findings)}`);