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 { newSmartAccount8130 } from 'viem/experimental/eip8130'
// secp256k1: the signer is a LocalAccount; ECRECOVER is the default authenticator.
const account = newSmartAccount8130({
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 { newSmartAccount8130, toP256Signer } from 'viem/experimental/eip8130'
const signer = toP256Signer({ privateKey: '0x…' })
// signer.sign returns r || s || x || y || preHash (129 bytes)
// signer.authenticator = canonical P-256 authenticator
const account = newSmartAccount8130({ 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 { newSmartAccount8130, toWebAuthnSigner } from 'viem/experimental/eip8130'
const credential = await createWebAuthnCredential({ name: 'vibes' })
const signer = toWebAuthnSigner(toWebAuthnAccount({ credential }))
const account = newSmartAccount8130({ 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,
sendCalls8130,
} from 'viem/experimental/eip8130'
// The same signer signs transactions…
const hash = await sendCalls8130(client, { account, calls, gas })
// …and config changes (authorize / revoke).
const change = await account.change(
[authorizeActor(key.p256({ x, y }), { scope: actorScope.sender })],
{ chainId: client.chain.id, sequence },
)

Pricing non-K1 signatures

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

estimateGas8130 with a verifierts
import { canonicalAuthenticators, estimateGas8130 } from 'viem/experimental/eip8130'
const gas = await estimateGas8130(client, {
sender: account.address,
calls: [[{ to, data }]],
// Price the sender auth from the RIGHT blob shape:
senderAuthVerifier: canonicalAuthenticators.passkey,
})