Grants & spending controls
Grants & spending controls
What a grant is
A grant is “an actor that may spend from this wallet.” Three kinds today:
account_default— the implicit grant for the account owner, used whenever the user is signed in directly (dashboard session, dashboard JWT).api_key— one per API key. Created automatically when the key is created; deleted (revoked) when the key is.oauth— one per OAuth grant issued to an agent via/oauth/authorize. Revoking the grant kills every token tied to it.
Each grant has its own row in spending_controls. That’s what makes “Claude can spend 500 KES/day, but my server cron can spend 50,000 KES/day” expressible: they’re different grants, each with its own caps.
What controls do
Five checks run before any wallet hold is placed, in this order:
daily_limit— sum of today’s confirmed + pending debits + this call’s price must be ≤ limitper_transaction_limit— this call’s price must be ≤ limitblocked_capability_slugs— if the slug is on the list, rejectallowed_capability_slugs— if a non-empty allow-list is set and the slug isn’t on it, rejectrequire_escalation_above— if the price exceeds the threshold, returnescalation_required(the agent must request a human approval before this call goes through)
Any rejection produces a 402 with X-LAP-Rejection-Reason: <code> and a Lipafy error code matching the failed check.
Setting controls
That sets account-level (legacy) controls. To set controls per grant (per agent), pass grant_id:
Resolution order
When the gateway checks controls for a request, it tries:
- The specific
grant_idof the credential the caller authenticated with - The account-level (
grant_id IS NULL) row, as a fallback “house rules” - If neither exists, no controls — the call goes through
This means grant-specific controls fully override the account default. Set a grant row with active: false and a request authenticating via that grant runs with no controls at all (an explicit choice, not a fallback) — so use that toggle deliberately.
Why this beats “limit per account”
The naïve model — one cap per account — means giving an agent any access risks your full daily budget. With grants:
- Different agents get different caps without separate accounts
- Revoking one agent doesn’t disrupt others
- Audit shows exactly which credential drove which spend
- Approvals attach to a specific grant, not the whole account
The same primitive scales from “I give Claude 500 KES/day” to “this company gives each of its 200 employees a Lipafy grant with a different cap, and they all share one wallet.”
What controls don’t do
- They don’t replace balance checks. If your wallet has 50 KES and the call costs 100, you get
insufficient_balanceregardless of controls. - They don’t apply at scheduled-intent fire time differently than at live-call time — the controls active when the call fires are what matter. A user who tightens controls between scheduling and firing will see the scheduled call fail.
- They don’t override mandates. An intent mandate’s own conditions (slug patterns, per-fulfillment cap, total budget) are checked independently and in addition.
