> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usecompassai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Session keys

> How the agent gets scoped permission, how every call is checked on-chain, and how the owner takes the permission back.

> The agent does not hold the user's USDC. It holds a session key — an additional signer the Diamond authorizes to call a specific set of functions under a specific set of policy constraints. Every call the session key signs is validated on-chain before USDC moves.

This page covers the session key mechanism. For the broader policy model
the session key enforces, see [Policy engine](/architecture/policy-engine).
For how the session key fits into the Diamond, see
[Diamond account](/contracts/diamond-account).

## What a session is

A session is a record stored on the user's Diamond. Each user's Diamond has
its own session table; there is no global agent registry.

| Field               | Type       | Meaning                                          |
| :------------------ | :--------- | :----------------------------------------------- |
| `agent`             | `address`  | The session-key signer.                          |
| `expires_at`        | `uint64`   | Unix timestamp. Session is dead after this time. |
| `allowed_selectors` | `bytes4[]` | Function selectors the agent may call.           |

The session table lives in the Security facet's storage and is read on
every `validateUserOp` call.

## Registration — owner-only

A session is created by the owner calling the Security facet:

```text theme={null}
SecurityFacet.registerSession(agent, expiresAt, allowedSelectors)
                ↑ onlyOwner
```

Three properties matter:

* **Owner-only.** Only the user's EOA can grant or modify a session. The
  agent's own session key has no permission to call `registerSession`.
* **Bounded expiry.** `expiresAt` is required. There is no "permanent"
  session — every session naturally expires and must be renewed by the
  owner.
* **Selector whitelist.** `allowedSelectors` enumerates which functions the
  agent may call from the Diamond. Selectors not in this list are
  unreachable by the agent.

## Validation — every UserOp

Every ERC-4337 UserOp the agent submits passes through `validateUserOp` on
the Account4337 facet. The validator checks the call against **two** layers:
the session key whitelist (this page) and the on-chain policy
([policy engine](/architecture/policy-engine)).

The full check sequence for an agent-signed UserOp:

<img src="https://mintcdn.com/compassai/Qq7UDvjWweIau4hP/images/session-keys-diagram.svg?fit=max&auto=format&n=Qq7UDvjWweIau4hP&q=85&s=e3072a5b02888d3f797823e8fdc22eb1" alt="session keys diagram" className="rounded-lg" width="1500" height="800" data-path="images/session-keys-diagram.svg" />

1. **Signature recovery.** Recover the signer address from the UserOp
   signature.
2. **Owner short-circuit.** If the signer is the owner EOA, allow anything.
3. **Session lookup.** Look up the signer in the session table:
   * Reject if no session exists.
   * Reject if `block.timestamp >= expires_at`.
   * Reject if the UserOp's entry selector is not in `allowed_selectors`.
4. **Policy check.** Decode the call arguments and run them against the
   on-chain policy stored on the Diamond:
   * Is the `(target_protocol, target_chain)` pair in
     `protocol_whitelist`?
   * Is the target chain in `chain_whitelist`?
   * Does the target protocol's risk score satisfy the user's `risk_band`?
   * Does the amount fit `per_route_cap_usdc`?
   * Does the rolling 24-hour total stay under `daily_cap_usdc`?
5. **EntryPoint prefund.** Pay the EntryPoint gas prefund from the Diamond
   if any is owed.

Any failure in steps 3 or 4 reverts the UserOp before execution. The
session key cannot produce a call that bypasses any of these checks — the
Diamond facet itself enforces them.

## On-chain and off-chain — same rules, two places

The policy attached to the user's Diamond is the **single source of
truth**. Both the off-chain deterministic loop and the on-chain session
key validation read from it:

* **Off-chain (deterministic loop).** Reads a cached copy of the policy
  from the indexed Diamond storage. Used to filter out plans before they
  ever become UserOps. This is a performance optimization — it saves gas
  and gives the dashboard structured rejection messages.
* **On-chain (session key).** Reads the policy directly from Diamond
  storage on every UserOp. This is the actual safety boundary. Even if
  the off-chain layer is compromised or bypassed, the on-chain check is
  what physically prevents an out-of-policy call from executing.

If the off-chain cache is ever stale relative to on-chain, the on-chain
check is what wins. The system is **fail-closed**: a UserOp that the
off-chain layer would have rejected, but somehow reached the chain, will
revert at `validateUserOp` rather than execute.

## What the session key cannot do

| Action                                | Why it's impossible                                                                                                                                      |
| :------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Withdraw to a non-owner address.**  | Transfer selectors that move USDC to external addresses are never included in `allowed_selectors`. Only the owner can sign for them.                     |
| **Grant itself more permissions.**    | `registerSession` and all other Security facet mutations are `onlyOwner`.                                                                                |
| **Survive past `expires_at`.**        | Every UserOp re-checks expiry against `block.timestamp`. A session is dead the moment the timestamp passes.                                              |
| **Reach a non-whitelisted protocol.** | Two gates: (1) the protocol's facet has to be registered on the Diamond, and (2) the session has to include that facet's selectors. Both are owner-only. |
| **Exceed a policy parameter.**        | Even calls to whitelisted selectors are checked against `risk_band`, caps, and whitelists on every UserOp.                                               |
| **Upgrade the Diamond.**              | `DiamondCut` selectors are never in `allowed_selectors`. Upgrades go through the separate [authority model](/contracts/authority-upgrade-model).         |

The selector whitelist alone would be insufficient — an agent could
otherwise call a whitelisted `supply` function with any amount, on any
chain. The on-chain policy check is what makes the whitelist meaningful:
it ties each call to the user's specific rules.

## Revocation — the kill switch

The owner can revoke a session at any time with a single transaction:

```text theme={null}
SecurityFacet.revokeSession(agent)
                ↑ onlyOwner
```

Effect:

* The agent's session is removed from the session table.
* All future UserOps signed by that agent address fail at step 3 of
  validation.
* The Diamond's USDC and existing positions are untouched. Revocation
  affects future calls only.

Revocation is final for the revoked agent address — a new session has to
be registered (with a new key) to delegate again.

### Two independent kill switches

Compass exposes two ways to stop an agent from acting on your account:

| Layer                        | Effect                                                                              | Recovery                       |
| :--------------------------- | :---------------------------------------------------------------------------------- | :----------------------------- |
| **Off-chain pause**          | The deterministic loop's scheduler skips your account; no new plans are dispatched. | Resume from the dashboard.     |
| **On-chain `revokeSession`** | The session key is invalidated at the contract level. UserOps revert at validation. | Owner registers a new session. |

The off-chain pause is convenient — one click, instant — and is sufficient
under normal operation. The on-chain revoke is the **hard** stop: it works
even if the Compass backend is offline, compromised, or simply ignoring
your pause request.

If you have any reason to distrust the off-chain layer, use the on-chain
revoke. It does not depend on Compass infrastructure.

## Next steps

<CardGroup cols={2}>
  <Card title="Policy engine" icon="shield-check" href="/architecture/policy-engine">
    The full rule set every UserOp is checked against.
  </Card>

  <Card title="Authority & upgrade model" icon="git-pull-request" href="/contracts/authority-upgrade-model">
    The separate path for shipping new facets — and how to revoke it.
  </Card>

  <Card title="Diamond account" icon="file-code" href="/contracts/diamond-account">
    The smart account architecture this session key lives on.
  </Card>

  <Card title="Trust & security model" icon="shield" href="/overview/trust-security">
    The big picture in plain language.
  </Card>
</CardGroup>
