Guide

Keys & scopes

An EIP-8130 account is controlled by a set of actors. You rotate ownership by applying a signed config account-change that authorizes new actors and/or revokes existing ones. Because a config change is just an accountChanges entry, it rides inside a normal transaction.

Actors and keys

Each actor is { actorId, authenticator }. Build them with the key.* helpers:

key buildersts
import { key } from 'viem/experimental/eip8130'
key.k1('0xowner…') // secp256k1 (native ecrecover)
key.p256({ x, y }) // P-256 public key
key.passkey({ x, y }) // WebAuthn / FIDO2 passkey
key.delegate('0xotherAccount') // signatures for another account act for this one
key.trustedExecutor('0xmgr…') // driven via executeBatch by msg.sender, not a signature

Scopes

A scope is a bitmask of grants. Combine flags with toScope. Scope 0x00 is unrestricted admin: config changes, lock, delegation, and ERC-1271 signing all ride on admin scope (there is no separate signature or config bit).

ConceptMeaning
actorScope.sender (0x01)May originate transactions with the account as sender.
actorScope.policy (0x02)Actor is gated to its policy manager. Set automatically when you pass a policy; do not set it by hand.
actorScope.nonce (0x04)May use sequenced 2D nonce keys. Without it, the actor is restricted to nonce-free (expiring) transactions.
actorScope.selfPayer (0x08)May pay for its own transactions (payer == sender).
actorScope.sponsorPayer (0x10)May sponsor other accounts (payer != sender).
0x00 (admin)Unrestricted full owner. Omit scope (or pass 0) for a full-owner key.

authorizeActor attaches a scope, optional expiry, and optional policy to a key:

authorizeActorts
import { actorScope, authorizeActor, key, toScope } from 'viem/experimental/eip8130'
authorizeActor(key.p256({ x, y }), {
// Combine scope flags; 0 (omitted) = unrestricted admin.
scope: toScope(actorScope.sender, actorScope.nonce),
// Optional expiry (unix seconds); 0 / omitted = no expiry.
expiry: BigInt(Math.floor(Date.now() / 1000) + 86_400),
})

Read the config sequence

Every config change is signed against the account's next local sequence. Read it first to avoid sequence-mismatch rejections from a stale cache.

getConfigSequence8130ts
import { getConfigSequence8130, getEip8130Deployment } from 'viem/experimental/eip8130'
const { accountConfiguration } = getEip8130Deployment(client.chain.id)!
const { local: sequence } = await getConfigSequence8130(client, {
accountConfiguration,
account: account.address,
})

The returned local value is the per-chain sequence. The action also returns a multichain sequence for replayable chain_id 0 changes that propagate to every chain, covered in Multichain.

Authorize, revoke, rotate

Sign a change with account.change(...), then include it in a transaction's accountChanges. revokeActor accepts an actor or a raw actorId. Combine authorize + revoke in one change to rotate a key atomically:

Atomic rotation (config-only tx)ts
import {
actorScope,
authorizeActor,
key,
revokeActor,
sendCalls8130,
} from 'viem/experimental/eip8130'
// Authorize the new key and revoke the old one in one atomic change.
const rotate = await account.change(
[
authorizeActor(key.k1('0xnewOwner…'), { scope: actorScope.sender }),
revokeActor(key.k1('0xoldOwner…')),
],
{ chainId: client.chain.id, sequence: Number(sequence) },
)
const hash = await sendCalls8130(client, {
account,
accountChanges: [rotate],
calls: [], // config-only transaction
gas: 200_000n,
})

Just-in-time vs. immediate

  • Immediate: land the change now with a config-only transaction (calls: []), as above.
  • Just-in-time: attach the change to a transaction that also does work, so the new key is authorized and used in the same send.
Authorize + act in one txts
// Add a key and use it in the same transaction.
const hash = await sendCalls8130(client, {
account,
accountChanges: [addKey],
calls: [{ to, data }],
gas: 300_000n,
})

Rotate during deployment

For a delegated EOA, you can delegate and install new keys in the very first transaction, no separate account handle required:

Delegate + add key on first usets
import {
actorScope,
authorizeActor,
canonicalEip8130Deployment,
key,
sendCalls8130,
toEoa8130Account,
} from 'viem/experimental/eip8130'
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'
const account = toEoa8130Account(privateKeyToAccount(generatePrivateKey()))
const addP256 = await account.change(
[authorizeActor(key.p256({ x, y }), { scope: actorScope.sender })],
{ chainId: client.chain.id, sequence: 0 },
)
// Delegate the EOA and install the P-256 key in the very first transaction.
const hash = await sendCalls8130(client, {
account,
accountChanges: [
account.delegate(canonicalEip8130Deployment.accounts.default),
addP256,
],
calls: [],
gas: 300_000n,
})