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:
import { key } from 'viem/experimental/eip8130'
key.k1('0xowner…') // secp256k1 (native ecrecover)key.p256({ x, y }) // P-256 public keykey.passkey({ x, y }) // WebAuthn / FIDO2 passkeykey.delegate('0xotherAccount') // signatures for another account act for this onekey.trustedExecutor('0xmgr…') // driven via executeBatch by msg.sender, not a signatureScopes
A scope is a uint16 bitmask of grants. Combine flags with toScope. Scope 0x0000 is unrestricted admin: config changes, lock, delegation, and ERC-1271 signing all ride on admin scope (there is no separate signature or config bit).
| Concept | Meaning |
|---|---|
| actorScope.sender (0x01) | May originate transactions with the account as sender. |
| actorScope.policy (0x02) | Actor is gated to its policy manager. Session keys use POLICY | NONCE (always grant NONCE for the primary path). |
| actorScope.nonce (0x04) | May use sequenced 2D nonce keys. Without it, the actor is restricted to nonce-free (expiring) transactions. Always set this for session keys. |
| actorScope.selfPayer (0x08) | May pay for its own transactions (payer == sender). |
| actorScope.sponsorPayer (0x10) | May sponsor other accounts (payer != sender). |
| 0x0000 (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:
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 live local sequence. Read it with getConfigSequence8130 immediately before signing. Never hardcode it.
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:
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.
// 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:
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,})Account locks
Locking freezes configuration changes and delegation, and unlocking is time-delayed (unlockDelay), giving the owner a window to react to a compromised key. Locked accounts are also eligible for elevated mempool rate limits. Lock changes are signed like actor changes: hash with hashLockChange8130, sign with an admin key (authenticator || data), then submit lockCall / initiateUnlockCall. They are local-channel only and consume the live local config sequence.
import { concatHex } from 'viem'import { canonicalAuthenticators, getConfigSequence8130, getLockStatus8130, hashLockChange8130, initiateUnlockCall, lockCall, sendCalls8130,} from 'viem/experimental/eip8130'
const { local } = await getConfigSequence8130(client, { account: account.address,})const unlockDelay = 3600 // seconds (uint16, 1…65535)
// 1) Hash + sign with an admin key → authenticator || dataconst digest = hashLockChange8130({ account: account.address, chainId: client.chain.id, op: 'lock', unlockDelay, sequence: Number(local),})const signature = await admin.sign({ hash: digest }) // 65-byte K1const auth = concatHex([canonicalAuthenticators.k1, signature])
// 2) Submit the signed lock changeawait sendCalls8130(client, { account, calls: [lockCall({ account: account.address, unlockDelay, auth })], gas: 300_000n,})
const status = await getLockStatus8130(client, { account: account.address })// status.locked, status.unlockDelay, …
// Later: initiate a delayed unlock (digest uses op: 'unlock', unlockDelay: 0)const unlockDigest = hashLockChange8130({ account: account.address, chainId: client.chain.id, op: 'unlock', unlockDelay: 0, sequence: Number((await getConfigSequence8130(client, { account: account.address, })).local),})const unlockAuth = concatHex([ canonicalAuthenticators.k1, await admin.sign({ hash: unlockDigest }),])await sendCalls8130(client, { account, calls: [initiateUnlockCall({ account: account.address, auth: unlockAuth })], gas: 250_000n,})Read state with getLockStatus8130 / isLocked8130. Why locks matter for throughput: Protocol · Authenticators & locks.