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
| Concept | Meaning |
|---|---|
| POLICY bit | Gates the actor to its policy_manager. Not operational; cannot ERC-1271-sign raw hashes. |
| policy_commitment | keccak256(params): signed and opaque. The protocol never interprets spend limits or allowlists. |
| policy_manager | The only contract the actor may call. Anything else → ActorPolicyViolation. |
| Not admin | Never 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.

- Authorize. The account signs an actor change registering the session key:
actorIdderived from the key,SCOPE_POLICYset,policy_manager = manager,policy_commitment = keccak256(binding). That signed change is the entire authorization. There is no separate install step. - 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). - Resolve actor. The manager reads the acting
actorIdfrom the tx-context precompile. - 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. - 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.
- 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 thepolicy_commitmentof that account's binding. BecauseapplySignedAccountChangesis signature-gated, anyone (including the service) can submit those signed changes; the account never needs to send a transaction. - Batch pull. The service calls
executeForMany(bindings[N], executionData[N])as a plain EVM transaction. No 8130 dispatch is involved. - Unforgeable identity. The manager derives
actorId = bytes20(msg.sender)from the caller, not from the payload. - Re-add protocol guarantees. Per entry the manager checks what protocol dispatch would have guaranteed:
policy_manager == thisfor 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. - 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, andresults[]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:
| Concept | Meaning |
|---|---|
| Sponsor | Default 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_PAYER | Authorize 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 gas | Builders 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

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.