Guide
Session keys
A session key is a policy-gated actor: a key authorized with a restricted scope and a policy commitment. The protocol forces every call it makes onto a PolicyManager, which enforces spend limits and allowlists, then drives the account. Hand out a scoped key without granting ownership.
The flow
- Author a
SessionPolicyconfig: the allowlists and spend limits. - Bind it to the account with
defineSessionPolicyto get the commitment, theauthorizeActorpolicy, and the install call. - Authorize + install the key in one transaction. The install initializes the binding and MUST land before the key's first use.
- Use the key: it sends an
executeCall, and its only reachable target is the manager.
Author + bind
import { parseUnits } from 'viem'import { defineSessionPolicy, encodeSessionPolicyConfig,} from 'viem/experimental/eip8130'
const session = defineSessionPolicy({ account: account.address, policyConfig: encodeSessionPolicyConfig({ // ≤ 100 USDC per week. tokenLimits: [ { token: usdc, limit: parseUnits('100', 6), period: 7n * 86_400n }, ], // Only transfer(address,uint256) on the USDC contract. callScopes: [ { target: usdc, selectorRules: [{ selector: '0xa9059cbb' }] }, ], }),})
session.commitment // the policy commitment stored on the actorsession.actorPolicy // pass to authorizeActor(key, { scope, policy })Selector rules may bind recipients for the standard ERC-20 selectors (transfer, transferFrom, approve):
import { encodeSessionPolicyConfig } from 'viem/experimental/eip8130'
encodeSessionPolicyConfig({ callScopes: [ { target: usdc, // recipients bind the standard ERC-20 selectors (transfer/transferFrom/approve). selectorRules: [{ selector: '0xa9059cbb', recipients: [payroll] }], }, ],})Authorize + install
Authorize the session key with a restricted scope (a policy-bearing actor must not be admin) and ride the install call in the same transaction. Read sequence with getConfigSequence8130 first.
import { actorScope, authorizeActor, key, sendCalls8130,} from 'viem/experimental/eip8130'
const sessionKey = key.p256({ x, y })
const change = await account.change( [ authorizeActor(sessionKey, { scope: actorScope.sender, policy: session.actorPolicy, }), ], { chainId: client.chain.id, sequence: Number(sequence) },)
const hash = await sendCalls8130(client, { account, accountChanges: [change], // The install initializes the binding; it MUST land before first use. calls: [session.installCall(sessionKey.actorId)], gas: 300_000n,})Use the session key
Build the account with the session signer and send an executeCall. The manager verifies the action against the committed policy, then executes it. A transfer that exceeds the limit, or targets a contract/selector outside the allowlist, reverts inside the manager, so the key can only ever act within its committed policy.
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem'import { encodeSessionPolicyAction, newSmartAccount8130, sendCalls8130, toP256Signer,} from 'viem/experimental/eip8130'
// Same account address, driven by the session key.const sessionAccount = newSmartAccount8130({ signer: toP256Signer({ privateKey: sessionPrivateKey }), salt: accountSalt,})
const transfer = encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [recipient, parseUnits('10', 6)],})
const hash = await sendCalls8130(client, { account: sessionAccount, calls: [ session.executeCall( encodeSessionPolicyAction({ target: usdc, data: transfer }), ), ], gas: 250_000n,})Subscriptions
A subscription is just a session key whose policy encodes a per-period limit (for example 4.99 USDC every 30 days). Because the key holds its own nonce lane, the merchant does not coordinate a shared account nonce: when a charge is due it fires the transaction on a nonce-free or dedicated channel. There is no per-charge approval, and cancelling is a single revokeActor change.
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem'import { encodeSessionPolicyAction, newSmartAccount8130, nonce, sendCalls8130, toP256Signer,} from 'viem/experimental/eip8130'
const sessionAccount = newSmartAccount8130({ signer: toP256Signer({ privateKey: sessionPrivateKey }), salt: accountSalt,})
const charge = encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [merchant, parseUnits('4.99', 6)],})
// No nonce to read or reserve, just fire when the charge is due.const hash = await sendCalls8130(client, { account: sessionAccount, calls: [ session.executeCall( encodeSessionPolicyAction({ target: usdc, data: charge }), ), ], gas: 250_000n, ...nonce.nonceless({ expiresIn: 600 }),})See Nonces & throughput for channel selection and Protocol · Subscriptions for the rationale.
Read remaining budget
Render a live budget view for a policy-gated key with getSessionSpend8130, which combines the configured limit and current-period spend for a token.
import { getSessionSpend8130 } from 'viem/experimental/eip8130'
const { allowance, spent, remaining, periodEnd } = await getSessionSpend8130( client, { commitment: session.commitment, token: usdc },)Paying for gas
A session key is typically POLICY-only and cannot touch account ETH, so it relies on a sponsor. Add SELF_PAYER only as a deliberate opt-in (a griefing key could then burn the balance on fees), or use an ERC-8168 payer service that accepts spend-limit gas. See Gas & payers.