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

  1. Create a smart account (sponsored create bumps the local config sequence to 1).
  2. Author + bind a SessionPolicy with defineSessionPolicy.
  3. Authorize the PolicyManager as a K1 operational actor (key.k1(manager)) and the session key in one signed actor change on the local channel, at the live sequence. No install step.
  4. Use the key via PolicyManager.execute(binding, action). Declare scope only at authorize; do not redeclare it on the session account handle.
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 authorizes the PolicyManager as a live K1 operational actor.
What happens under the hood when a session key spends: the PolicyManager resolves the acting actor, verifies the committed policy, and drives the account as a K1 operational actor.

Author + bind

defineSessionPolicyts
import { parseUnits } from 'viem'
import {
defineSessionPolicy,
encodeSessionPolicyConfig,
getEip8130Deployment,
} from 'viem/eip8130'
const deployment = getEip8130Deployment(client.chain.id)!
const { policies } = deployment
const session = defineSessionPolicy({
account: account.address,
policy: policies.sessionPolicy,
manager: policies.manager,
validUntil: BigInt(Math.floor(Date.now() / 1000) + 30 * 86_400),
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 actor
session.actorPolicy // pass to authorizeActor(key, { scope, policy })
session.executeCall // build a gated call to send at execute time
// No install — the full PolicyBinding is passed at execute time.

Selector rules may bind recipients for the standard ERC-20 selectors (transfer, transferFrom, approve):

Recipient allowlistts
import { encodeSessionPolicyConfig } from 'viem/eip8130'
encodeSessionPolicyConfig({
callScopes: [
{
target: usdc,
// recipients bind the standard ERC-20 selectors (transfer/transferFrom/approve).
selectorRules: [{ selector: '0xa9059cbb', recipients: [payroll] }],
},
],
})
Allow native ETH spendts
import { parseEther, zeroAddress } from 'viem'
import { encodeSessionPolicyConfig } from 'viem/eip8130'
// Native ETH is fail-closed: a key can move ETH (value > 0) only with a
// zero-address tokenLimit. Absent → no ETH (reverts NativeValueNotAllowed).
encodeSessionPolicyConfig({
// ≤ 0.1 ETH per day.
tokenLimits: [
{ token: zeroAddress, limit: parseEther('0.1'), period: 86_400n },
],
})

Authorize

Register the PolicyManager as a K1 operational actor (key.k1(policies.manager), actorScope.operator) 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 getConfigSequence 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).

Authorize manager + session key (no install)ts
import {
actorScope,
authorizeActor,
getConfigSequence,
key,
} from 'viem/eip8130'
import { createPayerClient, sendSponsoredCalls } from 'viem/eip8168'
const sessionKey = key.k1(sessionSigner.address)
// PolicyManager is a drive-only contract: register it as a K1 operational
// actor (actorId = pad(manager), authenticator = address(1)).
const manager = key.k1(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.
// The keystore is enshrined — no address argument.
const { local } = await getConfigSequence(client, {
account: account.address,
})
// Authorize manager + session key in ONE change. No install step.
const change = await account.change(
[
authorizeActor(manager, { scope: actorScope.operator }),
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 isActor / getActorConfig 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.

Spend within policyts
import {
canonicalAuthenticators,
toAccount,
} from 'viem/eip8130'
// Signer + address only — do NOT redeclare scope on this handle.
// prepare reads getActorConfig; with POLICY|NONCE it uses a sequenced nonce.
const sessionAccount = toAccount({
signer: sessionSigner,
address: account.address,
authenticator: canonicalAuthenticators.k1,
})
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:

  1. Session key on the account: authorize with POLICY | NONCE and fire each charge as an 8130 tx on its sequenced channel. No shared-nonce coordination across merchants.
  2. External caller: each account grants the service address as an EXTERNAL_POLICY_AUTHENTICATOR actor. The service then calls executeForMany as a plain EVM transaction and pulls from many accounts in one batch, with per-account revert isolation.
Recurring charge (session-key path)ts
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 getSessionSpend, which combines the configured limit and current-period spend for a token.

getSessionSpendts
import { getSessionSpend } from 'viem/eip8130'
const { allowance, spent, remaining, periodEnd } = await getSessionSpend(
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.