BUY-SIDE

Spend Guard

Spend Guard is the buy-side of the Fidacy engine. Where the sell-side asks whether a merchant should accept an agent payment, Spend Guard asks whether the consumer actually authorized this purchase — within the limits they set, for the merchant they intended, on the rail they approved. It runs outside the model, enforces the consumer’s mandate in code, and returns a signed, auditable verdict before any money moves.

How to use Spend Guard

Off by default and no new endpoint. Pass a spending_mandate alongside the AP2 mandate in your normal POST /v1/assess call. When the field is absent the response is byte-identical to today — the buy-side gate does not run.

POST/v1/assess
curl -X POST https://api.fidacy.com/v1/assess \
  -H "Authorization: Bearer fky_live_…" \
  -d '{
    "mandate": {
      "vct": "urn:ietf:params:ap2:payment",
      "payee": { "id": "merch_acme", "name": "Acme Office Supplies" },
      "payment_instrument": { "type": "card_debit" },
      "payment_amount": { "amount": 4999, "currency": "USD" }
    },
    "spending_mandate": {
      "policy_owner": "consumer",
      "subject": { "user_id": "usr_123", "agent_id": "kya_thumbprint_…" },
      "per_transaction_max": { "amount": 10000, "currency": "USD" },
      "daily_max":   { "amount": 50000,  "currency": "USD" },
      "monthly_max": { "amount": 200000, "currency": "USD" },
      "velocity": { "window": "1h", "max_count": 5 },
      "allow": { "merchants": ["merch_acme", "merch_staples"] },
      "deny":  { "merchants": ["merch_casino"] },
      "require_human_confirmation_above": { "amount": 7500, "currency": "USD" },
      "rails_allowed": ["card_debit", "card_credit"],
      "expires_at": "2026-12-31T23:59:59Z"
    }
  }'

The top-level decision is the composed verdict (most restrictive of sell-side and buy-side). The additive spend_guard object carries the buy-side verdict alone so you can distinguish why a decision changed.

{
  "decision": "approve",
  "score": 14,
  "assessmentId": "asmt_…",
  "spend_guard": {
    "evaluated": true,
    "decision": "approve",
    "reasons": [],
    "unevaluable": [],
    "deferred": []
  }
}

The spending mandate schema

All monetary amounts are in minor units (integer) with an ISO-4217 currency code. All fields optional unless noted. Enforced now (stateless):

per_transaction_max

Amount exceeds cap → deny. Currency mismatch → review. Amount absent → review.

allow.merchants

Non-empty allowlist. Merchant not in list → deny.

deny.merchants

Explicit blocklist. Merchant matched by id or name → deny.

require_human_confirmation_above

Amount over threshold → review (forces human step-up regardless of sell-side).

rails_allowed

Instrument types matched against payment_instrument.type. Rail not in list → deny.

expires_at

Mandate past this instant → deny. Unparseable → review (fail-safe).

Enforced now (stateful) — aggregate the consumer’s own history, keyed to subject. Spend counts only when the final decision is approve.

daily_max

Prior approved spend in the last 24h + this purchase exceeds the cap → deny.

monthly_max

Prior approved spend in the current calendar month (UTC) + this purchase exceeds the cap → deny.

velocity

Approved-purchase count within the rolling window reaches max_count → review.

Subject required. daily_max, monthly_max, and velocity need a resolvable subject. Sent without one, the engine degrades to review (recorded in spend_guard.unevaluable) — never silently passed.

Decision → action

approve

Both sides cleared. Forward the signed mandate; receipt in riskPayloadJws.

review

A buy-side step-up threshold was crossed. Pause the agent; the engine creates a pending confirmation and emits spend.review_required, resolved with POST /v1/confirmations/{id}.

deny

A hard buy-side limit was breached. Block the payment and log spend_guard.reasons.

Composition invariant. The buy-side gate can only tighten the sell-side verdict, never relax it. Ambiguity (unknown amount, unreadable rail) always degrades toward review, never toward approve.

Limitations (current slice)

The AP2 payment mandate carries payee and payment_instrument but no MCC or category, so allow.mccs, allow.categories and deny.categories cannot be evaluated — they are recorded in spend_guard.unevaluable, never silently treated as a pass.

Mandate & confirmation endpoints

POST/v1/spending-mandates
GET/v1/spending-mandates
POST/v1/confirmations/{id}

Persist mandates and resolve human step-ups. A confirm records the spend and emits spend.confirmed; a deny emits spend.denied. Resolving an already-resolved confirmation returns 409, so a retry can never double-count the spend.

Spend webhooks

spend.review_required

A purchase composed to review and a pending confirmation was created.

spend.confirmed

A human resolved a confirmation with confirm.

spend.denied

A purchase composed to deny on the buy-side, or a confirmation was denied.

← PREVIOUS
API Reference
NEXT →
Webhooks