RULE SPEC v0.1

Fourteen rule kinds — the fourteenth is your own.

Every rule is a row in the database: kind + config JSON, scoped business-wide or to a single card, evaluated server-side on every intent, with a per-rule verdict persisted on the decision. Declarative depth is the bet: everything below is configuration, with no rule service for you to write, host, or operate.

rule CRUD
POST   /rules      { "kind": "...", "config": { ... }, "card_id": null }   # null = business-wide
GET    /rules
PATCH  /rules/{id}  { "enabled": false }
DELETE /rules/{id}

# a decline from ANY rule denies; a human-gate parks (pending_approval);
# partial approvals return the approved ceiling; every result carries its reason.

Merchant rules

merchant_lockLIVE IN THE ENGINE

Allowlist — the card only works at merchants you name.

{ "kind": "merchant_lock", "config": { "allow": ["pizzeria", "doordash"] } }
BEHAVIOR

Substring match on merchant name/domain. Not on the list → decline.

USE IT FOR

Pizza bot locked to food-delivery merchants; sourcing agent locked to Grainger + Uline.

merchant_blockLIVE IN THE ENGINE

Denylist — named merchants always decline.

{ "kind": "merchant_block", "config": { "deny": ["casino", "sweepstakes"] } }
BEHAVIOR

Matches → decline, even if everything else passes.

USE IT FOR

Company-wide 'never these merchants' policy under looser per-agent allowances.

mcc_blockLIVE IN THE ENGINE

Category control by MCC — allowlist or denylist of merchant category codes.

{ "kind": "mcc_block", "config": { "allow": ["5812", "5814"] } }
{ "kind": "mcc_block", "config": { "deny_codes": ["7995", "5993"] } }
BEHAVIOR

Allow mode: only listed MCCs pass. Deny mode: listed MCCs decline.

USE IT FOR

Allow restaurants only; block gambling and tobacco fleet-wide.

Amount rules

amount_ceilingLIVE IN THE ENGINE

Per-transaction maximum, with optional partial approval.

{ "kind": "amount_ceiling", "config": { "max_cents": 3000, "allow_partial": true } }
BEHAVIOR

Over the max → decline, or partial-approve up to the ceiling when allow_partial is set.

USE IT FOR

No single pizza order over $30; infra agent capped at $1,500 per purchase.

spend_capLIVE IN THE ENGINE

Cumulative spend cap over a rolling window — the sum, not the count.

{ "kind": "spend_cap", "config": { "max_total_cents": 20000, "window": "day", "allow_partial": true } }
BEHAVIOR

Projected total (spent + this purchase) over the cap → decline, or partial-approve the remaining headroom. Windows: day, week, month, or seconds.

USE IT FOR

Agent spends at most $200/day no matter how many purchases it splits across.

per_merchant_spend_capLIVE IN THE ENGINE

A rolling spend cap scoped to one merchant — a loose global cap and a tight per-merchant cap on the same card.

{ "kind": "per_merchant_spend_cap", "config": { "merchant": "doordash", "max_total_cents": 6000, "window": "day" } }
BEHAVIOR

Only counts spend at the named merchant over the window; over the cap → decline (partial-approve supported). Other merchants fall under your global caps.

USE IT FOR

$200/day overall, but never more than $60/day at any single delivery service — one card, one agent key.

Cadence rules

velocityLIVE IN THE ENGINE

Transaction count limit per rolling window.

{ "kind": "velocity", "config": { "max_count": 10, "window_sec": 3600 } }
BEHAVIOR

N-or-more purchases in the window → decline until it rolls off.

USE IT FOR

A stuck retry loop can't machine-gun 400 orders in a minute.

cooldownLIVE IN THE ENGINE

Minimum seconds between purchases — the runaway-loop brake.

{ "kind": "cooldown", "config": { "cooldown_sec": 300 } }
BEHAVIOR

A purchase within the cooldown of the last approved one → decline.

USE IT FOR

An agent that just bought lunch cannot buy lunch again 4 seconds later.

max_usesLIVE IN THE ENGINE

Lifetime use count. max_uses: 1 is a single-use card — as one config value, not a separate product.

{ "kind": "max_uses", "config": { "max_uses": 1 } }
BEHAVIOR

After N approved purchases, everything declines.

USE IT FOR

One-shot procurement: the card that buys exactly one laptop, then dies.

duplicate_charge_guardLIVE IN THE ENGINE

The retry-storm rule. Same merchant and same amount inside the window is treated as a duplicate fire, not a new decision.

{ "kind": "duplicate_charge_guard", "config": { "window_sec": 600 } }
BEHAVIOR

An identical merchant + amount within the window → decline. Built for how software fails, not how people spend.

USE IT FOR

A timeout after the charge already succeeded makes an agent pay again, verbatim, seconds later. Humans almost never do this; agents in a retry loop do.

Time rules

time_windowLIVE IN THE ENGINE

Allowed hours of day, timezone-aware, wraparound supported.

{ "kind": "time_window", "config": { "start_hour": 9, "end_hour": 18, "tz": "America/Chicago" } }
BEHAVIOR

Outside the window → decline. 22–6 works (overnight wraparound).

USE IT FOR

Procurement agent spends business hours only; scraper budget only at night.

The human gates

approval_thresholdLIVE IN THE ENGINE

Purchases above a threshold park for your sign-off instead of auto-deciding.

{ "kind": "approval_threshold", "config": { "above_cents": 50000 } }
BEHAVIOR

Over the line → status pending_approval. You approve or deny from the dashboard, the TUI, or one API call. Approving re-runs every other rule.

USE IT FOR

The infra agent can buy $80 of compute alone, but a $2,400 reserved instance waits for you.

new_merchant_reviewLIVE IN THE ENGINE

First purchase from a merchant this business has never bought from parks for review (or declines).

{ "kind": "new_merchant_review", "config": {} }
{ "kind": "new_merchant_review", "config": { "action": "decline" } }
BEHAVIOR

Unseen merchant → pending_approval (default) or decline. Once you approve a merchant once, it's known.

USE IT FOR

The agent-specific rule: a prompt-injected agent can't wire money to a brand-new 'merchant' behind your back; the first hit always crosses your desk.

Bring your own rule

webhook_checkLIVE IN THE ENGINE

Bring your own rule. APPEN POSTs the declared intent to your own HTTPS endpoint, and your service returns the decision — anything the spec doesn't cover.

{ "kind": "webhook_check", "config": { "endpoint_id": "whe_…", "fail_mode": "closed" } }
BEHAVIOR

A PAN-free, HMAC-signed request to your registered endpoint; your { decision: approve | decline | review } maps to pass / deny / review. SSRF-guarded, and fail-closed by default — a slow or unreachable endpoint declines, it can't wave spend through. Runs after the local rules pass, so a doomed intent is never sent out.

USE IT FOR

Your own fraud model, an inventory check, a per-customer budget in your database. Your service becomes a rule on the card, with no auth service to host.

Plus the budgeting layer

Envelopes sit above the rules: a cap per category (daily / weekly / monthly, with rollover), deny-by-default for categories with no envelope. Rules answer "is this purchase allowed?"; envelopes answer "is there budget left for this kind of thing?" Both run on every decision.

The frontier — designed, not live

Fourteen more kinds have full specs and wait their turn. Each is marked DESIGNED until it ships — none of these runs in the engine today, and none carries a live-money claim.

task_ttlDESIGNED

the card expires with the task; an expired card is inert by construction

refund_authority_splitDESIGNED

refund scope separated from spend scope on a key

retry_storm_breakerDESIGNED

N declines in a window freezes the card

scope_drift_guardDESIGNED

task scope declared at mint; out-of-scope intents park

price_sanityDESIGNED

amount checked against your own trailing median for that merchant

subscription_guardDESIGNED

unregistered recurring charges park for review

merchant_geoDESIGNED

country allow/deny on the declared merchant, before funding

delegation_limitDESIGNED

whether an agent key can mint child keys, and how much they get

runtime_attestationDESIGNED

key pinned to a model fingerprint; a silent swap parks the intent

merchant_hoursDESIGNED

a charge outside the merchant's own hours parks

receipt_requiredDESIGNED

unmet receipt obligations block the next load

dual_approvalDESIGNED

two humans must sign off above a threshold

owner_proximityDESIGNED

spend gated to the owner's location, via the companion app

swipe_geo_verifyDESIGNED

declared vs actual merchant geography reconciled after settlement

Where we're honest about the edges

  • Lithic ships a configurable declarative rule engine (Auth Rules v2) with persisted rule-result traces — for underwritten card programs (KYB + a program agreement), not individuals — and its ASA hook can express anything if you write, host, and operate your own low-latency authorization service. If you're a company willing to become a program, that's the right tool. APPEN's bet is that governed spend shouldn't require becoming one — our engine plays that role for a person with an API key.
  • Privacy.com's configurable surface is spend-limit durations, three card types, and pause/close. The depth gap is real, and it's the honest comparison to make.
  • — Several kinds here (timezone-aware hours, lifetime max-uses, cooldowns, async human approval queues, new-merchant review) exist because software fails differently than employees do. Each element exists somewhere; what no live product ships (verified July 2026) is the combination — self-serve for an individual, a declare-before-credential intent gate, fourteen explained-and-persisted rule kinds including your own via webhook, and handle-not-PAN by construction.

WHERE THIS RUNS TODAY

The rule engine decides every declared intent today — live in the API, the MCP server, and the demo. Enforcement on real card swipes begins at our BIN connection with our issuing partner, when the same pre-authorized, user-directed policies execute inside the network authorization step. Card issuance opens by invite once our issuing-partner sandbox is live.