Skip to content

The Four Clause Gate

Marcus Richards edited this page Oct 6, 2026 · 1 revision

The Four-Clause Gate

Attempt a against permit P is authorized iff:

amount(a) <= remaining(P)
  AND merchant(a) IN allowlist(P)
  AND now < expiry(P)
  AND NOT revoked(P)

remaining(P) = cap(P) − reserved(P) − captured(P)

Four clauses. No discretion. Code: PermitStore._evaluate() in permit/permit.py.

How the code evaluates it

  1. Amount guard (_validate_amount). The value must be an int, must not be a bool, and must be > 0. Otherwise the result is invalid_amount. check() writes a BLOCKED receipt and never raises.
  2. Permit lookup. A missing permit gives unknown_permit.
  3. Clauses, in this order: revoked → expiry → merchant → amount. The first failure is the reason.
  4. Lineage gate. For a sub-permit, every ancestor must exist, be unrevoked, and be unexpired. See Delegation and Sub-Permits.

The clauses use effective values after any tighten: the lower cap, the intersected allowlist, the sooner expiry.

eligible() vs check()

eligible() check()
Used by POST /api/permits/<id>/check, the approval probe the spend pipeline
Reserves cap No Yes, on ALLOWED (adds to reserved_cents, records an auth_id)
Writes a receipt No Yes: ALLOWED or BLOCKED
Concurrency — Re-evaluates under the per-permit lock, so parallel attempts cannot exceed the cap

Block reasons (from code)

Reason Meaning
invalid_amount Not a positive int
unknown_permit No such permit id
revoked Permit revoked
expired / tightened_expiry_passed now >= expiry (granted / tightened)
merchant_not_allowed Merchant not in the effective allowlist
over_remaining_cap / tightened_cap_exceeded Amount exceeds remaining (granted / tightened cap)
ancestor_revoked / ancestor_expired / ancestor_missing Lineage gate failed

The pipeline adds merchant_not_bound, invalid_approval and approval_not_approved. See Spend Pipeline.

Invariants

  • A BLOCKED attempt never reaches PayPal, not even the authorize call.
  • permit/permit.py must not import any PayPal client (tests/test_permit.py::test_no_paypal_imports_in_permit_module).
  • All money is integer cents.

HTTP note

/check and /spend reject a non-int or non-positive amount_cents with 400 before the gate runs. POST /api/permits and /delegate coerce cap_cents with int() first. See HTTP API Reference.

Clone this wiki locally