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.
| Concept | Meaning |
|---|---|
| ECRECOVER (K1) | privateKeyToAccount(pk). data = r || s || v (65 bytes). The default authenticator. |
| P-256 | toP256Signer({ privateKey }). data = r || s || x || y || preHash (129 bytes). |
| WebAuthn / passkey | toWebAuthnSigner(toWebAuthnAccount({ credential })). data = ABI-encoded (WebAuthnAuth, x, y). |
| Delegate | Another 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.
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.
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 automaticallyWebAuthn passkey
toWebAuthnSigner wraps Viem's toWebAuthnAccount so the assertion is ABI-encoded into the passkey authenticator data. Each signature is a fresh user gesture.
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 automaticallyOne signer, everything
The same signer produces sender_auth for transactions and the auth for config changes: nothing else changes between the two.
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.
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,})