Docs Product

KIFF Cards

Every other check in KIFF asks whether one action is allowed: is the entity in the right state, does this actor have the permission, are the parameters valid, does this need an approval. A card carries the answer to the one question no single action can answer, what has this agent already done, and is that enough?

An action can be correct in every way and still be the one that should not run, because of the twenty that came before it. That is what a card catches.

What a card is

A KIFF Card, a mandate in the API, the package and the schema, is a bounded, revocable instrument issued to one named agent:

refund-agent may run REFUND_ORDER, only while the order is PAID, up to a total of 500000 summed over the amount_cents parameter, each calendar day, until it is revoked.

What it says:

  • Who holds it: one agent. It must match the actor_id the agent already sends when it proposes an action. Each card has one named holder, and two agents never share a card: two agents doing the same work hold two cards, each with its own balance. One agent may hold several, see below, where the tightest binds.
  • What it may do: the actions it covers. An action you leave out is still governed by the contract; it just does not draw on this card.
  • Under what conditions: the entity states the grant is confined to. Leave them unset and the action’s own states apply, which is every state the domain already allows it in. A card narrows; it never widens.
  • How much, over what period: a quantity, either count (how many times those actions are authorized) or sum(<parameter>) (the total of one numeric parameter the actions declare), with a ceiling and a window: each calendar day, any rolling 24 hours, or any rolling hour.
  • When it stops: an optional expiry, or the moment an owner revokes it.

When a card is required

By default a card constrains without conferring: an agent with no card covering an action is unaffected, and the check is a no-op. That is what lets you adopt cards one agent at a time without the first one breaking something already running.

An action opts out of that by declaring requires_authority in the domain:

actions:
  - name: REFUND_ORDER
    allowed_states: [PAID, PARTIALLY_REFUNDED]
    requires_authority: true

For that action, no card is a refusal rather than a no-op:

{
  "outcome": "blocked",
  "reasons": ["no_mandate_issued"]
}

That reason is its own, deliberately. The first integrator to meet it has wired everything correctly, and a generic blocked would send them to audit their domain, their permissions and their state machine before discovering that nothing has been issued yet.

It is declared per action, not per tenant: the author of the domain knows which consequences are worth an instrument, and a tenant-wide switch would make that judgement for every action at once.

What happens at the ceiling

The action is refused. It is not sent for approval.

“At the ceiling” means the proposed action would exceed what the card has left for the window, not that the card is empty. A €2,000 refund against €1,000 of remaining authority is refused, and a €500 one right after it is allowed, same card, same window, same reason code for the first.

This is deliberate. A cap that produces an approval request someone clicks through at the end of a long day is not a cap; it is a notification with extra steps, and it re-creates exactly the rubber-stamping that let the decision drift to the machine in the first place.

A proposal the card cannot cover comes back as:

{
  "outcome": "blocked",
  "reasons": ["mandate_limit_reached"],
  "message": "this action exceeds the remaining authority under mandate refund-agent-card: sum(amount_cents) limit 500000 per calendar_day reached: 300000 used, this action draws 250000"
}

The engine’s own words in that message are the precise rule: 300000 used of 500000, and an action drawing 250000 does not fit, €2,000 of authority is still on the card for something smaller.

If an agent needs more room, you raise the ceiling: one deliberate act, recorded with who made it, rather than approving past it one action at a time. That is the same decision, made visible and attributable instead of ambient.

Some properties worth knowing

A card narrows; it never confers. It selects from what the domain and its permissions already allow and caps how much of it happens. It cannot grant an action the domain does not have, or legalise one in a state it forbids. This is the opposite of a capability system.

Several cards can cover one action, and the tightest binds. Each is checked; the first that refuses decides. There is no precedence rule to reason about, and no way to widen an agent’s authority by adding another card.

A card that cannot be read refuses. If the ledger is unreachable, the action is blocked rather than allowed, because allowing means acting on authority nobody could confirm. Cards marks such a balance unknown rather than showing a comforting zero, an unused card and an unreadable one are very different facts.

A revoked card refuses; it does not disappear. Revoking does not return the agent to “unbounded” for an action that requires one, and re-issuing the same name does not quietly bring it back: a proposal under a revoked card is blocked with mandate_expired, and its statement stays readable.

Draws are recorded when an action is authorized, not when it executes. KIFF decides; your system executes, and reports no outcome back. An authorization your code chose not to run still counts against the window. A retry of the same proposal does not draw twice.

A limit binds the approved path too. When an operator grants an approval, the re-decision runs the same check. Approval does not buy authority.

The domain’s observe posture

A card is checked even when the domain is in observe. In that posture the decision is advisory and the caller continues its existing flow; the authorization still draws on the card, whether or not the caller ultimately executes it. KIFF never learns which, so what the statement shows is authorized authority, never confirmed spend.

Counting it anyway is exactly what lets you see what your agent would have drawn before you enforce anything. That is the recommended on-ramp: issue a card deliberately high, run for a week, read the statement, and set the real ceiling from your own traffic instead of from a guess.

(This is the domain’s observe posture, which still calls the decide API. Guard’s own observe mode is a different thing: it watches an agent’s tool calls without asking KIFF at all, so nothing is decided and no card is drawn.)

The statement

Cards in the dashboard shows, for each card: the ceiling, how much has been drawn in the current window, what remains, and every movement that got it there.

A movement names what it was for, REFUND_ORDER on ord_8812: carries how much it drew, and says how much of that limit’s window had been consumed once it landed. Where a card caps the same quantity over more than one window, each subtotal names its own: five an hour and twenty a day are both true of the same movement.

Two things the statement deliberately does not do:

  • It never claims execution. Every movement is an authorization. KIFF decided; your system ran the action afterwards and does not report back, so an action you authorized and never ran still drew.
  • It never lists refusals. A refusal moved no balance, so it is not a movement. Refusals live in the domain’s activity, which the card links to, filtered to its holder.

Below the movements, Issued and changed is the card’s history: what its ceiling was at each issuance, and who changed it. Re-issuing a card overwrites the row it lives in, so this is the only record of what the authority used to be.

Changing the ceiling

Change ceiling on the card opens the issuance form with its current terms, because a change is a re-issue under the same name. Draws already recorded still count, so raising a ceiling adds only the difference rather than opening a fresh window, and the revision records who made the change.

Setting one

Cards → Issue a card in the dashboard. The form offers the actions your domain declares, the entity states each of them allows, and only the parameters it types as a number, so a quantity that cannot be summed is not offered in the first place.

Or over HTTP, with a management credential:

curl -sS -X POST https://api.kiff.dev/v1/me/mandates \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "id": "refund-agent-card",
        "subject": "refund-agent",
        "domain": "card-refund",
        "grants": [{"action": "REFUND_ORDER", "entity_states": ["PAID"]}],
        "aggregates": [
          {"quantity": "sum(amount_cents)", "limit": 500000, "window": "calendar_day"}
        ],
        "valid_until": "2027-01-01T00:00:00Z"
      }'

Issuing and revoking require a management identity, the signed-in account owner or an admin. An agent’s own runtime key is refused. A credential that could grant itself authority would make every card advisory.

What this is not

  • Not a payment card. No money moves through KIFF. A payment card knows a merchant and an amount; a KIFF Card knows that this order is refundable, and counts whatever the consequence is, three DELETE_CUSTOMER an hour is the same instrument as €5,000 a day.
  • Not a rate limit. A rate limit protects your infrastructure from volume. A card bounds consequence: what an agent is permitted to do, in the units the action is measured in.
  • Not a spend tracker. Nothing here reads your ledger or your bank. It counts what KIFF authorized, and says so on the statement.
  • Not portable. A card is enforced by your KIFF tenant. Signing one so a third party could verify it independently is a later question.

See also

  • Decisions, the proposal flow a limit is checked in, and the outcomes it can produce.
  • Govern, the per-control evidence view, and the observe/enforce posture a limit is checked under either way.
  • Protected Controls, how one consequential action is expressed, which is what a card’s grants name.