Guide

Signing

Every authorization in EIP-8130 is an auth blob: an authenticator address followed by its data. The authenticator decides how the data is verified, so one account can be controlled by secp256k1, P-256, passkey, or delegate keys. Viem's signer helpers produce the right data for each.

Auth blobs

A sender_auth takes one of two forms. On the EOA path (from unset) it is a raw 65-byte secp256k1 signature and the sender is recovered from it. On the configured-actor path (from set) it is authenticator || data, where the authenticator validates the data.

ConceptMeaning
ECRECOVER (K1)privateKeyToAccount(pk). data = r || s || v (65 bytes). The default authenticator.
P-256toP256Signer({ privateKey }). data = r || s || x || y || preHash (129 bytes).
WebAuthn / passkeytoWebAuthnSigner(toWebAuthnAccount({ credential })). data = ABI-encoded (WebAuthnAuth, x, y).
DelegateAnother account's signer + canonicalAuthenticators.delegate. Validates a signature produced for the linked account.

secp256k1 owner

A K1 signer is just a Viem LocalAccount; its type is detected automatically and ECRECOVER is the default authenticator.

K1 signerts
import { privateKeyToAccount } from 'viem/accounts'
import { newSmartAccount } from 'viem/eip8130'
// secp256k1: the signer is a LocalAccount; ECRECOVER is the default authenticator.
const account = newSmartAccount({
signer: privateKeyToAccount('0x…'),
})

P-256 key

toP256Signer signs over the digest directly (no re-hash) and carries the P-256 authenticator, so it drops straight into an account.

P-256 signerts
import { newSmartAccount, toP256Signer } from 'viem/eip8130'
const signer = toP256Signer({ privateKey: '0x…' })
// signer.sign returns r || s || x || y || preHash (129 bytes)
// signer.authenticator = canonical P-256 authenticator
const account = newSmartAccount({ signer }) // P-256 detected automatically

WebAuthn passkey

toWebAuthnSigner wraps Viem's toWebAuthnAccount so the assertion is ABI-encoded into the passkey authenticator data. Each signature is a fresh user gesture.

Passkey signerts
import {
createWebAuthnCredential,
toWebAuthnAccount,
} from 'viem/account-abstraction'
import { newSmartAccount, toWebAuthnSigner } from 'viem/eip8130'
const credential = await createWebAuthnCredential({ name: 'vibes' })
const signer = toWebAuthnSigner(toWebAuthnAccount({ credential }))
const account = newSmartAccount({ signer }) // WebAuthn detected automatically

One signer, everything

The same signer produces sender_auth for transactions and the auth for config changes: nothing else changes between the two.

Transactions and config changests
import {
actorScope,
authorizeActor,
key,
sendTransaction,
} from 'viem/eip8130'
// The same signer signs transactions…
const hash = await sendTransaction(client, { account, calls, gas })
// …and config changes (authorize / revoke).
const change = await account.change(
[authorizeActor(key.p256({ x, y }), { scope: actorScope.operator })],
{ chainId: client.chain.id, sequence },
)

Signing messages (ERC-1271)

An EIP-8130 account signs messages and typed data too: account.signMessage and account.signTypedData return an ERC-1271 SignedMessageEnvelope signature. By default the envelope is multichain (sigType 0x02, bound to chainId 0), so it verifies for the account on every chain. Verify with core client.verifyMessage / client.verifyTypedData, which route through the account's isValidSignature.

Sign and verify a messagets
import { privateKeyToAccount } from 'viem/accounts'
import { newSmartAccount } from 'viem/eip8130'
const account = newSmartAccount({ signer: privateKeyToAccount('0x…') })
// Multichain ERC-1271 signature by default (sigType 0x02, binds chainId 0).
const signature = await account.signMessage({ message: 'gm' })
const typedSig = await account.signTypedData(typedData)
// Verify via core ERC-1271 (calls the account's isValidSignature):
const valid = await client.verifyMessage({
address: account.address,
message: 'gm',
signature,
})

The wire form is sigType(1) || authenticator(20) || data. Use the standalone signMessageEnvelope / signTypedDataEnvelope helpers for non-K1 signers or to pick a channel: signatureType.multichain (0x02, the default) binds chainId 0; signatureType.local (0x01) binds block.chainid.

Envelope helpers and channelsts
import { signMessageEnvelope, signatureType } from 'viem/eip8130'
// Standalone helper: P-256 / WebAuthn / delegate signers sign the SAME
// envelope via their authenticator. Default sigType is 'multichain'.
const multichain = await signMessageEnvelope({
signer,
account: account.address,
message: 'gm',
authenticator, // defaults to the signer's authenticator
})
// Chain-bound (local) signature: sigType 0x01 binds block.chainid.
const local = await signMessageEnvelope({
signer,
account: account.address,
message: 'gm',
sigType: 'local',
chainId: client.chain.id,
})
// Envelope layout: sigType(1) || authenticator(20) || data
signatureType.local // 0x01 (this chain only)
signatureType.multichain // 0x02 (valid on all chains; the default)

For canonical Keystore validation that also tells you which actor signed, use validateSignature, which returns { valid, actorId, scope }. For a not-yet-deployed account, wrap the signature with wrapCounterfactualSignature (ERC-6492) so verifiers can validate it before deployment.

Validate and counterfactualts
import {
validateSignature,
wrapCounterfactualSignature,
} from 'viem/eip8130'
// Canonical Keystore validation: returns WHICH actor/scope signed.
const { valid, actorId, scope } = await validateSignature(client, {
account: account.address,
message: 'gm',
signature,
})
// Not-yet-deployed account? Wrap as ERC-6492 so verifiers can
// validate it before deployment.
const wrapped = wrapCounterfactualSignature({
signature,
userSalt,
code,
initialActors,
})

Pricing non-K1 signatures

Authentication gas depends on the authenticator's blob shape, so gas estimates must know which authenticator will sign. Pass senderAuthAuthenticator (and payerAuthAuthenticator for a payer) matching the signing authenticator.

estimateGas with an authenticatorts
import { canonicalAuthenticators, estimateGas } from 'viem/eip8130'
const gas = await estimateGas(client, {
sender: account.address,
calls: [[{ to, data }]],
// Price the sender auth from the RIGHT authenticator:
senderAuthAuthenticator: canonicalAuthenticators.passkey,
})