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
- Create a smart account (sponsored create bumps the local config sequence to
1). - Author + bind a
SessionPolicywithdefineSessionPolicy. - Authorize the PolicyManager (trusted executor) and the session key in one signed actor change on the local channel, at the live sequence. No install step.
- Use the key via
PolicyManager.execute(binding, action). Declare scope only at authorize; do not redeclare it on the session account handle.

Author + bind
import { parseUnits } from 'viem'import { defineSessionPolicy, encodeSessionPolicyConfig, getEip8130Deployment,} from 'viem/experimental/eip8130'
const deployment = getEip8130Deployment(client.chain.id)!const { policies } = deployment
const session = defineSessionPolicy({ account: account.address, policy: policies.sessionPolicy, manager: policies.manager, 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 })// No install — the full PolicyBinding is passed at execute time.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
Register the PolicyManager as a trusted executor and the session key with a restricted scope in one change. Always grant NONCE for session keys so they use a sequenced channel (POLICY | NONCE). Do not start from nonceless for the primary path. Read the live local sequence with getConfigSequence8130 immediately before signing. Never hardcode it. On hosted Vibenet, the payer co-signs with mode: 'sign'; broadcast with the public RPC (https://rpc.vibes.base.org).
import { actorScope, authorizeActor, getConfigSequence8130, key,} from 'viem/experimental/eip8130'import { createPayerClient, sendSponsoredCalls } from 'viem/experimental/eip8168'
const sessionKey = key.k1(sessionSigner.address)const manager = key.trustedExecutor(policies.manager)
// Always grant NONCE for session keys (sequenced channel). Declare scope ONCE// at authorize — do not redeclare it on the session account handle later.// Optional: OR actorScope.selfPayer to pay gas from account ETH.const sessionScope = actorScope.policy | actorScope.nonce
// Read the LIVE local sequence — never hardcode it.const { local } = await getConfigSequence8130(client, { account: account.address, accountConfiguration: deployment.accountConfiguration,})
// Authorize manager + session key in ONE change. No install step.const change = await account.change( [ authorizeActor(manager, { scope: actorScope.sender }), authorizeActor(sessionKey, { scope: sessionScope, expiry, policy: session.actorPolicy, }), ], { chainId: client.chain.id, sequence: Number(local) },)
// Hosted Vibenet payer co-signs and broadcasts (default mode: 'send').const payer = createPayerClient({ url: 'https://vibes.base.org/api/vibenet/account/payer',})const { transactionHash: hash } = await sendSponsoredCalls(client, { account, payerClient: payer, accountChanges: [change], calls: [{ to: account.address, value: 0n, data: '0x' }], gas: 2_000_000n, // floor: payer estimates from calls only context: { flow: 'transact' },})// If the tx lands, account changes applied (invalid changes cannot land).// Prefer isActor8130 / getActorConfig8130 to confirm; poll briefly if needed.Use the session key
Just use it. Build a session account handle with signer + address only (no redeclared scope: prepare reads on-chain getActorConfig and picks nonce mode from chain truth). Send executeCall; the full binding rides with the call. The manager verifies the action against the committed policy, then executes it.
import { canonicalAuthenticators, to8130Account,} from 'viem/experimental/eip8130'
// Signer + address only — do NOT redeclare scope on this handle.// prepare reads getActorConfig; with POLICY|NONCE it uses a sequenced nonce.const sessionAccount = to8130Account({ signer: sessionSigner, address: account.address, authenticator: canonicalAuthenticators.k1, accountConfigAddress: deployment.accountConfiguration,})
const executeCall = session.executeCall({ target: usdc, value: 0n, data: transferData,})
const { transactionHash: hash } = await sendSponsoredCalls(client, { account: sessionAccount, payerClient: payer, calls: [executeCall], gas: 2_000_000n, context: { flow: 'transact' },})Subscriptions
A subscription is a policy-gated spend with a per-period limit (for example 4.99 USDC every 30 days). Two shapes:
- Session key on the account: authorize with
POLICY | NONCEand fire each charge as an 8130 tx on its sequenced channel. No shared-nonce coordination across merchants. - External caller: each account grants the service address as an
EXTERNAL_POLICY_AUTHENTICATORactor. The service then callsexecuteForManyas a plain EVM transaction and pulls from many accounts in one batch, with per-account revert isolation.
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem'
const charge = encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [merchant, parseUnits('4.99', 6)],})
// Same session key (POLICY|NONCE): sequenced channel, not nonceless.// External-caller batch pulls use executeForMany instead (see Protocol).const executeCall = session.executeCall({ target: usdc, value: 0n, data: charge,})See Nonces & throughput for channel selection and Protocol · External caller flow for the batch-subscription path.
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 | NONCE and relies on a sponsor for gas. OR 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. See Gas & payers.