Protocol

Policies and session keys

The use case is simple: give a particular key (or an external service) small, bounded access to an app or action without handing over full account authority. Session keys are not a special transaction type: they are actors with the POLICY scope bit, a commitment, and exactly one allowed target. The protocol stays dumb; PolicyManager contracts stay expressive.

What a policy actor is

ConceptMeaning
POLICY bitGates the actor to its policy_manager. Not operational; cannot ERC-1271-sign raw hashes.
policy_commitmentkeccak256(params): signed and opaque. The protocol never interprets spend limits or allowlists.
policy_managerThe only contract the actor may call. Anything else → ActorPolicyViolation.
Not adminNever scope 0x0000. Optional SELF_PAYER / NONCE. Optional actor expiry.

Why opaque commitments?

  • No protocol upgrades for app policy: games, payroll, trading limits evolve in contracts, not in the EIP.
  • Mempool stays simple: nodes do not need to understand ERC-20 selectors or weekly budgets to admit a tx; they check actor_config + later execution gates.
  • Stable invalidation: changing a policy means changing commitment / manager slots (or revoke), which is an explicit actor_config invalidator, not hidden logic inside wallet bytecode.

Why a single policy_manager target?

The account-level gate is one comparison: call.to == policy_manager. That is cheap, auditable, and forces session traffic through a contract that can enforce the commitment (spend, selectors, recipients).

Alternatives considered and rejected for the protocol layer:

  • Inline allowlists in actor_config: unbounded state, constant EIP churn, every node must understand every policy schema.
  • Wallet-only enforcement: returns to “simulate the account” for safety; apps can lie; mempool cannot help.
  • Multi-target protocol allowlists: more SLOADs and schema in the system contract; little gain vs one manager that dispatches.

Why never admin?

A session key that can rewrite actors (or lock the account) is not a session key; it is an owner. Admin is exactly scope == 0x0000. Policy actors are never admin, so privilege separation holds without trusting the PolicyManager to “please not call config.”

Actor-based (session key) flow

The account-acting path: a session key on the account originates an 8130 transaction, the protocol dispatches as the account, and the manager learns the acting actor from the transaction-context precompile.

Account policies flow: a policy-scoped caller invokes the PolicyManager, which reads the acting actorId from the tx-context precompile, fetches the actor's policy_commitment from the Keystore, calls the committed policy after verifying the commitment, receives the call plan to execute, and calls executeBatch on the account. The account checks the caller holds TRUSTED_EXECUTOR before executing.
Default account policy flow. The protocol only enforces the single-target gate and the opaque commitment; the PolicyManager reads the acting actor from the tx-context precompile, verifies the committed policy, and drives the account via a trusted executor. Policy logic lives in the manager, not in protocol.
  1. Authorize. The account signs an actor change registering the session key: actorId derived from the key, SCOPE_POLICY set, policy_manager = manager, policy_commitment = keccak256(binding). That signed change is the entire authorization. There is no separate install step.
  2. Use. The protocol authenticates the key (expiry checked here), the scope gate forces call.to == manager, and dispatches the call as the account (msg.sender == account).
  3. Resolve actor. The manager reads the acting actorId from the tx-context precompile.
  4. Re-authenticate binding. The caller supplies the full binding; the manager recomputes its commitment and requires it to equal the live Keystore[account][actorId].policy_commitment. One check authenticates config, validity window, and owner.
  5. Enforce + execute. The policy validates the config from calldata, consumes spend accounting keyed by commitment, and returns the call plan; the manager forwards it to the account.

External caller flow (subscriptions at scale)

Sometimes the actor is not a key on the account at all, for example a subscription service that wants one batch call pulling from many accounts, rather than each account running its own session key. Identity must come from the caller, not from 8130 dispatch.

  1. Per-account grant. Each account signs an actor change granting the service's address: actorId = bytes32(bytes20(serviceAddress)), authenticator = EXTERNAL_POLICY_AUTHENTICATOR (a no-code sentinel: recognized, but cannot send 8130 txs or drive the account directly), SCOPE_POLICY, policy_manager = manager, and the policy_commitment of that account's binding. Because applySignedAccountChanges is signature-gated, anyone (including the service) can submit those signed changes; the account never needs to send a transaction.
  2. Batch pull. The service calls executeForMany(bindings[N], executionData[N]) as a plain EVM transaction. No 8130 dispatch is involved.
  3. Unforgeable identity. The manager derives actorId = bytes20(msg.sender) from the caller, not from the payload.
  4. Re-add protocol guarantees. Per entry the manager checks what protocol dispatch would have guaranteed: policy_manager == this for that (account, actorId), the actor is not expired, and the supplied binding hashes to the live signed commitment. Then the identical policy enforcement path as the session-key flow.
  5. Isolated failures. Each entry runs in its own revert boundary: a revoked, expired, or over-budget account is skipped with ExecutionSkipped, the other accounts settle, and results[] reports per-account success.

The important property for either subscription path is that there is nothing to coordinate across accounts or charges. On the session-key path, grant NONCE and charge on a nonce-free (time-bounded) channel (or a dedicated sequenced channel). On the external-caller path, the service fires one plain EVM batch. Cancel is one actor change (revoke or rotate the commitment), which is an explicit actor_config invalidator.

Paying for gas with a session key

A policy actor can originate transactions, but gas still has to come from somewhere. There are four common paths:

ConceptMeaning
SponsorDefault for session keys: the account owner (or a payer service) co-signs payer_auth under a separate payer with SPONSOR_PAYER scope. The session key only signs sender_auth; it never touches the account’s ETH balance.
SELF_PAYERAuthorize the session key with POLICY | SELF_PAYER. It can then self-pay from the account’s ETH balance. The key still cannot move ETH except through gas, but a compromised or griefing key can burn the full balance on fees, so wallets should treat this as a deliberate opt-in.
Payer service (ERC-8168)A payer web service accepts off-chain payment (or a spend-limit budget) and co-signs payer_auth onchain. The session key stays POLICY-only; repayment or budget accounting lives in the service and/or a phase-0 settlement call.
Builder-native gasBuilders or sequencers MAY include transactions where gas is covered natively, e.g. an empty payer_auth with a recognized sentinel payer address. Chain- and builder-specific; not part of core 8130 validation.

Most session keys ship as POLICY | NONCE (always grant nonce scope for the primary path) and rely on a sponsor for gas. Full gas-abstraction paths (including dedicated gas keys and token repay) are in Gas & payers. See also Scopes, payers & phases · payer modes for how sender and payer auth compose.

Validation vs execution split

Other AA proposals run authentication, unstandardized authorization lookups, and an optional complex policy check all inside unpaid validation. EIP-8130 keeps validation to cryptographic authentication and known account-config authorization; the complex policy check is optional and happens at execution, where an actor gated by the policy system can only reach its policy target.
Other AA proposals push policy checks into unpaid validation. 8130 keeps validation to authentication plus known account-config authorization, and defers the policy gate to execution.

At mempool / auth time

Authorize actor, check restricted scope and expiry, confirm authenticator binding. Do not evaluate spend remaining.

At execution time

Account forces target = manager; manager checks commitment and executes (e.g. executeCall). Over-limit or wrong selector reverts inside the manager: user-visible failure, not a consensus surprise.