Guide

Gas & payers

An EIP-8130 transaction can name a payer: a second account that co-signs and pays gas. The sender authorizes the calls (sender_auth); the payer authorizes paying (payer_auth). This is the native mechanism behind sponsorship and pay-with-token, no bundler or EntryPoint.

Co-sign locally

When you hold the payer key (self-sponsor or your own backend), pass a payer signer to sendCalls8130. It signs payer_auth bound to the sender and sets the payer wire field. Set payer.address when the onchain payer account differs from the signing key.

Payer co-signts
import { privateKeyToAccount } from 'viem/accounts'
import { sendCalls8130 } from 'viem/experimental/eip8130'
const sponsor = privateKeyToAccount(process.env.SPONSOR_KEY as `0x${string}`)
const hash = await sendCalls8130(client, {
account, // signs sender_auth
calls: [{ to: recipient, data }],
gas: 250_000n,
payer: { account: sponsor }, // signs payer_auth, pays the fee
})

Whether a key may pay depends on its scope: SELF_PAYER funds the account's own transactions, while SPONSOR_PAYER funds others. The distinction and its risk model live in Gas & payers (protocol).

Estimating a sponsored transaction

Pass the payer address (and payerAuthVerifier if the payer uses a non-K1 key) so the node prices the payer authentication too.

estimateGas8130 with payerts
import { canonicalAuthenticators, estimateGas8130 } from 'viem/experimental/eip8130'
const gas = await estimateGas8130(client, {
sender: account.address,
calls: [[{ to: recipient, data }]],
payer: sponsor.address,
// Only needed if the payer uses a non-K1 key.
payerAuthVerifier: canonicalAuthenticators.k1,
})

Pay with a token

To charge the sender in an ERC-20 (e.g. USDV) while the payer fronts native gas, run a two-phase transaction: phase 0 transfers the token to the payer, phase 1 runs the user's calls. The payer co-signs because it can see its fee is paid in phase 0.

Payer services call phases: batch 0 commits a token transfer to the payer; batch 1 can revert without undoing that payment.
Token payment lands in phase 0 and stays committed even if a later phase fails. ERC-8168 covers ERC-20 gas payment and other action-for-sponsorship flows.
Two-phase token paymentts
import { encodeTokenTransfer } from 'viem/experimental/eip8168'
import { sendCalls8130 } from 'viem/experimental/eip8130'
const hash = await sendCalls8130(client, {
account,
calls: [
// phase 0: pay the payer in the token
[encodeTokenTransfer({ token, to: sponsor.address, amount: fee })],
// phase 1: the actual calls
[{ to: recipient, data }],
],
gas: 300_000n,
payer: { account: sponsor },
})

Payer services (ERC-8168)

When a service holds the payer key, ERC-8168 standardizes a payer_* JSON-RPC endpoint the wallet queries to sponsor a transaction or accept a token as gas. Viem exposes the client and helpers under viem/experimental/eip8168. sendSponsoredCalls fetches terms, selects an offer, and builds phases (including any phase-0 token transfer). On Vibenet the default mode: 'send' co-signs and broadcasts; pass mode: 'sign' if you want to submit the co-signed tx yourself on https://rpc.vibes.base.org.

sendSponsoredCalls (Vibenet)ts
import { createPayerClient, sendSponsoredCalls } from 'viem/experimental/eip8168'
// Vibenet: payer co-signs and broadcasts (default mode: 'send')
const payerClient = createPayerClient({
url: 'https://vibes.base.org/api/vibenet/account/payer',
})
const { transactionHash: hash } = await sendSponsoredCalls(client, {
account, // signs sender_auth; payer_auth is filled by the service
payerClient,
// mode defaults to 'send' — payer submits
// mode: 'sign' // optional: co-sign only, then eth_sendRawTransaction yourself
calls: [{ to: recipient, data }],
gas: 2_000_000n,
context: { flow: 'transact' },
// token, // omit to prefer a fully-sponsored offer
})

If the payer rejects with a recoverable reason (a higher token charge or gas limit), sendSponsoredCalls re-signs only when you approve each attempt via confirmRetry.

Confirming re-signsts
import { formatUnits } from 'viem'
import { sendSponsoredCalls } from 'viem/experimental/eip8168'
const { transactionHash: hash } = await sendSponsoredCalls(client, {
account,
payerClient,
// mode defaults to 'send'
calls: [{ to: recipient, data }],
token,
retries: 2,
// Each retry mints a NEW sender_auth (and a fresh passkey gesture),
// so nothing is re-signed without explicit approval.
confirmRetry: async (request) => {
if (request.kind === 'requote')
return confirm(`Pay ${formatUnits(request.paymentAmount, 6)} in token?`)
return confirm(`Raise gas limit to ${request.gasLimit}?`)
},
})

For full control, drive the primitives directly: payerClient.getTerms, selectPaymentOption, buildSponsoredCalls, then prepare, sign, and payerClient.sendTransaction (or payerClient.signTransaction + your own eth_sendRawTransaction). Decode rejections with parsePayerError.

Session keys and gas

  • A POLICY-only session key cannot touch account ETH; sponsor it, or route it through a payer service that accepts spend-limit gas.
  • Adding SELF_PAYER lets a session key self-fund gas, but a griefing key could then burn the balance on fees. Treat it as a deliberate opt-in. See Session keys.