Subchapter 4.3
references/payer-sponsorship.mdMarkdown6 KBView on GitHub
Sponsoring gas for 8130 accounts with a payer service — the path to gasless onboarding (account creation + first transaction with zero user ETH). For account creation and core concepts, read eip8130-accounts.md first.
A is a service that co-signs to pay gas (sponsored or in ERC-20). The hosted vibenet payer lives at and supports ERC-8168 modes:
payer_authhttps://api.vibes.base.org/api/vibenet/account/payermode: "send" (default) — payer co-signs and submits (payer_sendTransaction)mode: "sign" — payer co-signs only; you broadcast with eth_sendRawTransactionimport type { Hex } from "viem";
import { createPayerClient, sendSponsoredCalls } from "viem/eip8168";
import { waitForTransactionReceipt } from "viem/eip8130";
const payerClient = createPayerClient({
url: "https://api.vibes.base.org/api/vibenet/account/payer",
});
// Default mode:"send" — payer co-signs and broadcasts, resolving with an
// OBJECT: `{ transactionHash }`. Destructure it. (The declared return type says
// otherwise — see the note below — so TypeScript needs a cast.)
const { transactionHash: hash } = (await sendSponsoredCalls(client, {
account,
payerClient,
accountChanges: [account.createChange], // deploy rides in the first sponsored tx
calls: [{ to: account.address, value: 0n, data: "0x" }],
context: { flow: "transact" }, // budgets free grants per (sender, flow)
})) as unknown as { transactionHash: Hex };
const receipt = await waitForTransactionReceipt(client, { hash });No faucet call and no user ETH is needed anywhere in this flow — the payer’s
payer_auth makes the protocol debit gas from the payer, not the sender.
Gas only: if the sponsored calls transfer value, the account still has
to hold that value — a sponsored 0.001 ETH tip from a zero-balance account
reverts, a sponsored zero-value call does not.
Sponsored value transfers must be wallet-wrapped. sendTransaction
auto-routes value-bearing calls through the account, but sendSponsoredCalls
does not: calls: [{ to, value, data }] throws EIP-8130 calls cannot carry \value` on the wire(live-confirmed on the current branch). Wrap the phase withencodeWalletCalls` first — and remember the account must actually hold
the value:
import { encodeWalletCalls } from "viem/eip8130";
const { transactionHash } = (await sendSponsoredCalls(client, {
account,
payerClient,
calls: encodeWalletCalls({
account: account.address,
calls: [[{ to: recipient, value: parseEther("0.001"), data: "0x" }]],
})[0], // one phase → one wallet-routed executeBatch call
})) as unknown as { transactionHash: Hex };For self-submit (e.g. custom RPC / retry control), pass mode: "sign" — the
call then resolves with the signed raw transaction, which you broadcast yourself
with eth_sendRawTransaction.
The declared return type is wrong. SendSponsoredCallsReturnType is typed
as a union of hex strings (SendTransactionReturnType | SignTransactionReturnType),
but mode: "send" resolves with { transactionHash } at runtime —
live-confirmed against the hosted payer. Passing the raw result into
waitForTransactionReceipt fails at the RPC layer with
invalid type: map, expected 32 bytes, which points nowhere near the cause. The
union also isn’t assignable to Hex, so narrowing needs as unknown as.
Under the hood sendSponsoredCalls runs the ERC-8168 flow: fetch terms with
payer_getTerms, build and sign sender_auth with payer set and
payer_auth empty, then hand off via payer_sendTransaction (send mode) or
payer_signTransaction (sign mode). payer_getTerms +
payer_sendTransaction are the required pair every payer implements;
payer_signTransaction is optional (advertised in the terms’ methods).
Rejections come back as JSON-RPC error -32000 with a
data: { code, reason } envelope — branch on the string code (e.g.
BUDGET_EXHAUSTED, SENDER_LIMIT_REACHED, whose context includes a
validFor retry hint). Full types: src/eip8168/types.ts on
the fork branch.
account.createChange — only the first tx
carries it.actor is not bound right after a self-paid deploy. Account
config can propagate ~1 block behind the receipt (the same lag as
eth_getCode and config read-backs), and the payer validates against the
lagging state — a sponsored tx sent in that window is rejected with
EIP-8130 validation failed: actor is not bound, surfaced as viem’s
InvalidInputRpcError: Missing or invalid parameters. Neither message points
at timing. On 2s blocks this was live-reproduced (fails immediately, succeeds
~6s later); on today’s 200ms vibenet a sponsored send ~0.8s after the deploy
receipt was accepted first try. Either way: retry on actor is not bound
(sub-second gaps suffice now) or wait for the account’s code read-back.InvalidInputRpcError wrapping Broadcast failed … insufficient funds for gas * price + value: have <n> want <m> from an internal
http://base-client:8545 URL (live-hit 2026-08-21). The have figure is the
payer wallet’s balance. Diagnose with payer_getTerms → the payer
address → eth_getBalance; on the devnet the fix is to faucet-drip the payer
address itself. Also: the terms may advertise only a token (USDV) offer
while sponsored payer_sendTransaction still works — don’t gate on the offer
list.context.flow lets the hosted payer budget free grants per
(sender, flow) pair; pick a stable string per product surface
(e.g. "onboarding", "transact").src/eip8168 on the fork branch).