Guide

Sending transactions

An EIP-8130 transaction is an AA_TX_TYPE (0x79) envelope. sendTransaction fills nonce and fees, encodes calls, signs sender_auth (and payer_auth when sponsored), serializes, and submits via eth_sendRawTransaction. Use sendTransactionSync for a receipt in one round-trip.

Estimate gas

Unlike legacy EVM txs, the gas budget for an 8130 transaction is node-computed and required up front. estimateGas prices authentication from the shape of the auth blob (never a real signature), so you can estimate before signing. Pass a senderAuthAuthenticator hint (a canonical authenticator address) matching your signer.

estimateGasts
import { parseEther } from 'viem'
import {
canonicalAuthenticators,
estimateGas,
} from 'viem/eip8130'
const gas = await estimateGas(client, {
sender: account.address,
// Include createChange only on the first (deploying) transaction.
accountChanges: [account.createChange],
// Each inner array is one atomic executeBatch phase.
calls: [[{ to: recipient, value: parseEther('0.001') }]],
senderAuthAuthenticator: canonicalAuthenticators.k1, // .p256 / .passkey
})

For a policy-gated actor (a session key), also pass senderActorId so the node simulates against the intended acting actor and resolves the right policy, instead of the account's self-actor.

Deploy on first use

Include account.createChange in accountChanges to deploy and run the first calls in one transaction.

First transactionts
import { parseEther } from 'viem'
import {
canonicalAuthenticators,
estimateGas,
sendTransaction,
sendTransactionSync,
waitForTransactionReceipt,
} from 'viem/eip8130'
const calls = [{ to: recipient, value: parseEther('0.001') }]
const gas = await estimateGas(client, {
sender: account.address,
accountChanges: [account.createChange],
calls: [calls],
senderAuthAuthenticator: canonicalAuthenticators.k1,
})
const hash = await sendTransaction(client, {
account,
accountChanges: [account.createChange], // omit on later txs
calls,
gas: (gas * 120n) / 100n,
})
const receipt = await waitForTransactionReceipt(client, { hash })
receipt.status // call phases only — not account changes
receipt.eip8130.phaseStatuses // e.g. ['0x1'] per call phase
// Or, in one round-trip (eth_sendRawTransactionSync): returns the receipt.
const receiptSync = await sendTransactionSync(client, {
account,
accountChanges: [account.createChange],
calls,
gas: (gas * 120n) / 100n,
})

Reading receipts

Account changes and call phases have independent success. Do not treat receipt.status as "the whole transaction worked."

  • Account changes apply at phase −1, as a validity gate. If the transaction mined, those changes applied. If they were invalid (stale sequence, wrong actorId, revoking the signing key), the transaction is rejected at validation and never mines. There is no silent no-op.
  • receipt.status and allPhasesSucceeded cover call phases only. A 0x0 receipt can still mean the account was created, delegated, or rotated.
  • Confirm actor changes from ActorAuthorized / ActorRevoked logs, or a state read (isActor, getActorConfig, a bumped getConfigSequence). Poll about one block if state lags the receipt.
  • phaseStatuses is per call phase (0x1 / 0x0). The first 0x0 is the phase that reverted; later phases are skipped and also reported 0x0.

Follow-up transactions

Once deployed, drop account changes and pass calls only. The 2D nonce sequence is read automatically.

Subsequent sendts
const gas = await estimateGas(client, {
sender: account.address,
calls: [[{ to: token, data: transferData }]],
})
const hash = await sendTransaction(client, {
account,
calls: [{ to: token, data: transferData }],
gas: (gas * 120n) / 100n,
})

Native Core Integration

The examples above use the standalone sendTransaction(client, …) action. When your chain spreads eip8130ChainConfig (see Getting started), you can send the same AA transaction through core client.sendTransaction: the chain config routes an EIP-8130 account to a native 0x79 submit, and core client.getTransactionReceipt / waitForTransactionReceipt return the .eip8130 fields.

Core client sendts
// With eip8130ChainConfig spread into the chain, core client.sendTransaction
// submits a native AA tx (type 0x79) — gas is auto-estimated when omitted.
const hash = await client.sendTransaction({
account, // newSmartAccount(...) / toAccount(...)
calls: [{ to: recipient, value: parseEther('0.001') }],
accountChanges: [account.createChange], // first tx only
})
const receipt = await client.getTransactionReceipt({ hash })
receipt.eip8130.phaseStatuses // ['0x1']
// Note: sponsored (payer) sends are NOT available on core sendTransaction.

Transaction expiry tracking

Any EIP-8130 transaction with a non-zero expiry (sequenced or nonce-free) can no longer land once the chain's latest block timestamp passes it. Thread the signed validBefore out of sendTransaction with onTransaction, then pass it to waitForTransactionReceipt. The wait fails fast with TransactionExpiredError instead of spinning until timeout.

onTransaction + expiry waitts
import {
sendTransaction,
TransactionExpiredError,
waitForTransactionReceipt,
} from 'viem/eip8130'
// Thread the signed validBefore into the wait — reliable for any non-zero
// expiry (sequenced or nonce-free), including auto-computed nonceless windows.
let validBefore: bigint | undefined
const hash = await sendTransaction(client, {
account,
calls,
gas,
onTransaction: (tx) => {
validBefore = tx.validBefore
},
})
try {
const receipt = await waitForTransactionReceipt(client, { hash, validBefore })
receipt.eip8130.phaseStatuses
} catch (e) {
if (e instanceof TransactionExpiredError) {
// Tx can never land — resubmit with a fresh expiry.
} else {
throw e
}
}

Batching calls

A flat calls array runs as one atomic phase. Nested arrays control phases explicitly. The full model is in Calls & phases.

Atomic batchts
const hash = await sendTransaction(client, {
account,
calls: [
{ to: tokenA, data: approveData },
{ to: router, data: swapData },
],
gas: 400_000n,
})

A payer account co-signs and pays gas: a key you hold or an ERC-8168 service. Token payment (e.g. USDV) typically uses an early call phase; details in Gas & payers.

Payer co-signts
const hash = await sendTransaction(client, {
account,
calls: [{ to: recipient, data }],
gas: 250_000n,
payer: { account: sponsor },
})

Lower-level control

  • prepareTransactionRequest fills chain id, nonce sequence, and EIP-1559 fees.
  • account.signTransaction produces auth blobs and a serialized envelope for sendRawTransaction.

Metadata

Transactions may carry an opaque, signed metadata field for attribution, memos, or off-chain commitments. It is covered by the same sender signature and never affects execution. See Metadata & attribution for the field and the experimental encoding.