Abstract
ERC-7677 defines the paymasterService capability for EIP-5792, allowing apps to direct wallets to a sponsorship web service. This proposal extends that capability to EIP-8130 by defining payer_* RPC methods that wallets use when constructing EIP-8130 transactions. Apps pass the same paymasterService URL they already use; the wallet selects the appropriate RPC methods based on the transaction type it is constructing. A payer quotes full sponsorship, balance-funded sponsorship, or payment in a token at a binding rate, and co-signs the transaction the wallet builds. The same methods are served by app-provided services, wallet-configured payers, and chains' own nodes or sequencers, so a wallet can query several in parallel with its own gas estimation and pick one. Standing sponsorship balances let a wallet show a sender's remaining allowance or prepaid credit before it has an intent.
Motivation
EIP-8130 introduces native account abstraction where payers are first-class protocol participants that co-sign the transaction using the same authenticator || data model as senders, validated with PAYER scope. This differs fundamentally from the ERC-4337 paymaster model (no EntryPoint, no stub-data two-phase flow, the payer co-signs the exact transaction that lands onchain) and enables more efficient flows, including direct relay (the payer submits) or wallet submission of the co-signed transaction, and a protocol-enforced transaction expiry (valid_before).
ERC-7677 defines the pm_* RPC methods and lets an app tell a wallet "here is a sponsorship service URL" via the paymasterService capability. Those pm_* methods do not fit EIP-8130, so this proposal defines payer_* methods and reuses the same capability, so apps need no new integration: the wallet picks payer_* or pm_* based on the transaction type it is constructing.
This proposal supports three payment models: full sponsorship by an app, sponsorship drawn from a standing balance (e.g. a subscription allowance or prepaid credit), and token payment for gas.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Payer Service Interface
A payer service is a JSON-RPC endpoint serving these methods:
| Method | Support | Purpose |
|---|---|---|
payer_getTerms | REQUIRED | Quote payment options for a transaction intent, before the sender signs |
payer_sendTransaction | Per offer | Co-sign a sender-signed transaction and submit it |
payer_signTransaction | Per offer | Co-sign and return the co-signed bytes for the wallet to submit |
payer_getSponsorshipBalance | OPTIONAL | Read a sender's standing sponsorship balances, without an intent |
Each selectable offer lists the co-sign methods it supports in methods; an absent or empty methods means ["payer_sendTransaction"]. A wallet MUST NOT call a co-sign method that the offer it is using does not list. An offer MAY instead carry a preauthorized payerAuth, which the wallet places in the transaction itself, so no co-sign call is needed (see Preauthorized Payers); such an offer supports only the co-sign methods it lists explicitly.
The payer_* prefix names the responder's role, not a kind of server. An app's paymasterService URL, a wallet-configured payer, and a chain's own node or sequencer (on its standard JSON-RPC endpoint) all serve the same methods with the same response shapes, so a wallet has a single integration.
Flow.
- In parallel, the wallet calls
payer_getTermson each payer source it has and reads gas and fees from the chain (eth_estimateGasand fee fields), as it would for a self-paid transaction. - The wallet prices every option from its own gas numbers (see Transaction Construction), and the user or wallet picks one.
- The wallet builds and signs the transaction with the picked offer's
payer, then calls one of that offer's co-sign methods, or, for an offer withpayerAuth, setspayer_authitself and submits.
A caller that already knows an offer's terms, for example under a fixed agreement with the payer, MAY skip step 1. The payer still validates at co-sign and rejects with actionable data (such as a requote) if the terms no longer hold.
All durations in this specification are expressed as relative seconds measured from the time the response is sent (ttl, conditions.maxExpiry, RefundPolicy.window, PayerBalance.validFor). Relative durations keep a payer's response portable across clients with skewed clocks; a wallet caching a value records its own receipt time and counts down from there.
payer_getTerms
Returns sponsorship or token-payment terms for a transaction intent. Called before the sender signs. Payer services SHOULD evaluate the intent at this stage and return a sponsored_declined entry, rather than an offer, when they would not sponsor it.
payer_getTerms RPC Specification
// [params]
type GetTermsParams = {
chainId: `0x${string}`;
from: `0x${string}`;
calls: {
to: `0x${string}`;
value?: `0x${string}`;
data?: `0x${string}`;
}[];
gasLimit?: `0x${string}`;
// Transaction inputs, with the same names and shapes as the `eth_estimateGas` request for AA transactions
accountChanges?: Record<string, any>[]; // OPTIONAL account changes the transaction will carry (e.g. account creation), in the same form `eth_estimateGas` accepts for AA transactions; they can change gas substantially
senderActorId?: `0x${string}`; // OPTIONAL bytes32 Keystore actor that will sign `sender_auth`. Lets the payer simulate that actor's scope and policy and price its authentication; wallets SHOULD pass it when the actor is policy-gated
senderAuth?: `0x${string}`; // OPTIONAL placeholder `authenticator || data`, with `data` at the length a real signature will have. Priced by length; contents are ignored and never verified
preferredTokens?: `0x${string}`[]; // OPTIONAL ordering hint, not a filter: the payer SHOULD list these first in `tokens`, in this order, and MAY return other accepted tokens after them
fiatCurrency?: string; // OPTIONAL ISO-4217 code the wallet would like `fiatRate`s quoted in
context?: Record<string, any>; // opaque app context (e.g. policyId) from the paymasterService capability
};
type GetTermsResult = {
gasEstimate?: { // OPTIONAL advisory gas values shared by every offer, phase-0 gas included
gasLimit: `0x${string}`;
maxFeePerGas: `0x${string}`;
maxPriorityFeePerGas: `0x${string}`;
};
fiatCurrency?: string; // ISO-4217 code every `fiatRate` in this response is quoted in; REQUIRED when any `fiatRate` is present
options: PaymentOption[]; // best-first; an empty array means the payer has nothing to offer
};
// Selectable offers (`sponsored`, `token`) carry their own co-signer, endpoint,
// methods, validity, and conditions. A `token` offer lists its accepted tokens
// once under shared terms. `sponsored_declined` is never selectable.
type PaymentOption = SponsoredOffer | DeclinedSponsorship | TokenPaymentOffer;
type BaseOffer = {
payer: `0x${string}`; // co-signing account for THIS offer; the wallet sets it on the transaction
endpoint?: string; // co-sign URL for this offer; defaults to the called URL
methods?: ("payer_sendTransaction" | "payer_signTransaction")[]; // co-sign methods for this offer; absent or empty means ["payer_sendTransaction"], or none when `payerAuth` is present
payerAuth?: `0x${string}`; // preauthorized `payer_auth` (`authenticator || data`) the wallet sets itself; see Preauthorized Payers
ttl: number; // seconds this offer, `rate` included, may be cached and reused
conditions?: {
maxExpiry?: number; // relative, seconds from current time
maxGasLimit?: `0x${string}`; // hard cap on the transaction's gas units
maxCost?: `0x${string}`; // hard cap on gasLimit × maxFeePerGas, in wei
};
provider?: { // display info for the party behind this offer
name: string;
icon?: string;
};
};
type SponsoredOffer = BaseOffer & {
kind: "sponsored";
balance?: PayerBalance; // present when this sponsorship draws from an allowance/credit ("$5 of $10"); absent means fully paid by the sponsor
};
type DeclinedSponsorship = {
kind: "sponsored_declined";
code: SponsorshipDeclineCode; // see [Payer Error Codes](#payer-error-codes)
reason?: string; // human-readable detail for display/logs (e.g. "Monthly budget exhausted, resets 2026-07-01")
balance?: PayerBalance; // optional context — e.g. the exhausted allowance, whose `validFor` tells the wallet when to retry
gas?: { // present only for `GAS_EXCEEDS_LIMIT`
estimatedCost: `0x${string}`; // wei cost the payer estimated for this intent at current gas prices
maxCost: `0x${string}`; // per-tx ceiling in wei
};
provider?: { name: string; icon?: string }; // the would-be sponsor
};
type TokenPaymentOffer = BaseOffer & {
kind: "token";
tokens: TokenChoice[]; // ≥1 accepted tokens under this offer's shared terms
paymentMode?: "transfer" | "any"; // accepted phase-0 shapes; defaults to "transfer"
};
type TokenChoice = {
token: `0x${string}`;
symbol: string;
decimals: number;
rate: `0x${string}`; // binding price while the offer's `ttl` holds: token atomic units per 10^18 wei (one native unit), an 18-decimal fixed-point (WAD) value. The phase-0 credit MUST be at least the required amount (see Token Payment via Call Phases)
paymentAmount: `0x${string}`; // the required amount at the response's top-level `gasEstimate`; for display and for wallets that use `gasEstimate` as-is
paymentGas?: `0x${string}`; // advisory: gas the default phase-0 transfer adds to `gas_limit`. Payers SHOULD include it
feeRecipient?: `0x${string}`; // phase-0 destination; defaults to the offer's `payer`. With `payer` and `token`, identifies the choice at co-sign
fiatRate?: `0x${string}`; // optional display price of one whole `token` in `GetTermsResult.fiatCurrency`, 6-decimal fixed-point (e.g. "0xF4240" = 1.00)
refund?: RefundPolicy;
};PayerBalance is shared by payer_getTerms and payer_getSponsorshipBalance:
type PayerBalance = {
kind: "sponsorship" | "credit";
// ≥1 limits, all of which MUST hold for the next transaction to be covered
// (e.g. "$0.05 OR 3 txns weekly, whichever comes first"). The binding limit
// is the most-depleted: the smallest `available / limit` among entries that
// carry a `limit`. Wallets render it and MAY show the others.
limits: BalanceLimit[];
validFor?: number; // seconds until the snapshot changes: a refill for `sponsorship`, a lapse for `credit`
// Source attribution, REQUIRED from an aggregator fronting several services
payer?: `0x${string}`; // the payer/service address this balance is held with
endpoint?: string; // that service's endpoint URL
name?: string; // human-readable service name, e.g. "Coinbase One"
};
type BalanceLimit = {
unit: "asset" | "count"; // "asset" → amounts in base units of `asset`. "count" → integer counts of transactions / grants; `asset`/`symbol`/`decimals` are not used
available: `0x${string}`; // remaining within this axis
limit?: `0x${string}`; // total budget or cap for this axis, same units as `available`
spent?: `0x${string}`; // total lifetime spent on this axis (if tracked), same units as `available`
asset?: string; // REQUIRED when unit="asset": token contract address, "native", or an ISO-4217 currency code (e.g. "USD") for a fiat-denominated budget
symbol?: string;
decimals?: number;
};RefundPolicy describes when unused gas is returned to from. Its presence on a TokenChoice means refunds are offered for that choice; its absence means none are:
type RefundPolicy = {
window: number; // upper bound in seconds until the refund is settled (e.g. 86400 ≈ next day)
};The payer settles a refund as a separate ERC-20 transfer to from of the phase-0 credit minus the transaction's actual gas cost (priced through rate), paying that transfer's gas itself, within window seconds of inclusion. Settlement is not contract-enforced; see Security Considerations.
Field Notes
| Field | Notes |
|---|---|
options | Payment offers for this intent, best-first: sponsored (no phase 0), token (a phase-0 payment in one of tokens), and sponsored_declined (never selectable). A sponsored offer with a balance draws from an allowance or prepaid credit; without one, the sponsor pays in full. The wallet picks one selectable offer, and for a token offer one TokenChoice, and builds the transaction from that pair. |
sponsored_declined | Tells the wallet why sponsorship was not granted. Wallets MUST NOT build a transaction from it and SHOULD show it. A response MAY carry several, for example one per would-be sponsor in an aggregated response, told apart by provider; a payer SHOULD NOT emit two for the same provider. code comes from the decline subset of Payer Error Codes. balance gives context such as an exhausted allowance and when it refills. gas is present only for GAS_EXCEEDS_LIMIT: the payer's per-transaction ceiling and its cost estimate for this intent, so the wallet can show the gap. |
payer / endpoint | payer is the account the wallet sets on the transaction, and the phase-0 recipient when feeRecipient is absent. Offers MAY name different payers. endpoint defaults to the called URL; it is set only when the offer is co-signed elsewhere, for example by an aggregator's downstream payer. |
methods | The co-sign methods callable against the offer's (payer, endpoint); see Payer Service Interface. A relaying payer lists payer_sendTransaction (or omits methods), a pure co-signer lists payer_signTransaction, and a payer listing both lets the wallet choose. It is per offer because offers routed to different endpoints MAY expose different methods. |
payerAuth | Static payer_auth bytes valid for any transaction that satisfies the offer. The wallet sets payer_auth to exactly these bytes, signs, and submits through any node; the payer's authenticator decides validity onchain. See Preauthorized Payers. |
ttl | How long the offer's terms, including rate, may be cached and reused, in seconds. After it expires the wallet MUST call payer_getTerms again. It is independent of the transaction's onchain lifetime, which conditions.maxExpiry bounds. |
gasEstimate | Advisory gas values shared by every offer in the response, sized for the worst case (phase-0 gas included). A wallet MAY use them as-is or size gas itself. When present, they MUST satisfy every selectable offer's conditions; when no single set can, the payer MUST omit gasEstimate. |
conditions | Binding constraints for the offer. maxGasLimit caps gas_limit. maxCost caps gas_limit × max_fee_per_gas in wei and advertises the payer's per-transaction ceiling, so a wallet can preflight a cached offer. maxExpiry caps the transaction's lifetime in seconds (see Transaction Construction); payers SHOULD keep it to a few blocks, since they remain liable for gas until valid_before. |
tokens | One or more accepted tokens under the offer's shared terms. rate is binding while ttl holds (see Token Payment via Call Phases); payers SHOULD fold their own payer_auth cost and overhead into it. paymentAmount is the required amount at gasEstimate. paymentGas is advisory. feeRecipient MAY differ per token, for example to route fees to per-token treasuries. Several choices MAY share a token, such as refundable and non-refundable terms, only with distinct feeRecipients (see Co-Sign Disambiguation Invariant). |
paymentMode | "transfer" (default): phase 0 MUST be the single canonical IERC20.transfer(recipient, amount), which the payer verifies by inspection; other shapes are rejected with INVALID_TRANSACTION. "any": phase 0 MAY be any calls that credit recipient at least the required amount, such as transferFrom, a session-key module call, or a privacy-pool withdrawal; the payer verifies the credit by simulation. paymentGas describes the default transfer only. |
refund | When present, the payer returns the phase-0 credit minus the actual gas cost, priced through rate, to from in a separate transfer within window seconds of inclusion. When absent, the credit is final. |
balance | Advisory remaining budget (kind: "sponsorship") or prepaid credit (kind: "credit") behind a sponsored offer; the payer decides at co-sign. validFor is relative seconds until the snapshot changes: a refill for a sponsorship, a lapse for a credit. payer, endpoint, and name attribute the source in aggregated responses and SHOULD be omitted otherwise. |
provider | Display info for the party behind the offer, whose role follows from kind: the sponsor, the token-payment or swap provider, or the would-be sponsor that declined. icon MUST be a data URI (RFC 2397) of at least 96×96px; wallets MUST render SVG icons via <img> to prevent script execution. |
fiatCurrency / fiatRate | fiatRate is a display-only price of one whole token in fiatCurrency, 6-decimal fixed-point, valid for the offer's ttl. fiatCurrency echoes the wallet's requested currency when honored and otherwise names the payer's (conventionally "USD"); it is REQUIRED when any fiatRate is present. |
context | Opaque app context the wallet forwards verbatim to every payer method. Keys starting with _ are RESERVED for this protocol; app and policy keys SHOULD be namespaced (for example myApp.policyId). |
| Transaction inputs | accountChanges, senderActorId, and senderAuth have the same names and meaning as in the eth_estimateGas request for AA transactions, so a wallet can reuse one request object. They describe the transaction itself, which changes its gas and the payer's policy evaluation. Without actor hints, the payer assumes the sender's own secp256k1 key. The payer quotes against its current view of chain state. |
Co-Sign Disambiguation Invariant
The wallet does not echo which offer or TokenChoice it picked; the payer identifies it from the co-signed transaction. The co-sign fingerprint of a selectable construction is:
sponsored:(payer). There is no phase 0.token:(payer, token, recipient)of the phase-0 credit, determined by inspecting the transfer ("transfer") or by simulation ("any"). The amount is not part of it: once the choice is identified, the payer checks the credit against that choice's required amount, so a wallet can price from its own gas numbers or a cached offer.
Every selectable construction in a payer_getTerms response MUST have a unique fingerprint. A payer therefore MUST NOT return two sponsored offers under one payer (whether to draw on a balance is the payer's co-sign decision, not a separate offer), and MUST NOT return two token choices anywhere in the response that credit the same token to the same recipient under one payer. Distinct payer addresses, or for token choices distinct feeRecipients, resolve any collision; a separate recipient for refundable terms also keeps refund accounting apart. Declines and offers with payerAuth are never co-signed and are exempt. Wallets MAY reject a response that violates this invariant.
payer_getTerms Example Parameters
{
"chainId": "0x2105",
"from": "0xAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAaAa",
"calls": [
{
"to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"data": "0xa9059cbb..."
}
],
"gasLimit": "0xC350",
"preferredTokens": ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"]
}payer_getTerms Example Return Value (Full Sponsorship)
The payer advertises a per-transaction ceiling of 0xB5E620F48000 wei (2 × 10^14 wei, $0.40 at 2,000 USD per native unit) in conditions.maxCost, so a wallet caching this offer can preflight other intents against it.
{
"gasEstimate": {
"gasLimit": "0xC350",
"maxFeePerGas": "0x59682F00",
"maxPriorityFeePerGas": "0x59682F00"
},
"options": [
{
"kind": "sponsored",
"payer": "0xCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCc",
"methods": ["payer_sendTransaction", "payer_signTransaction"],
"ttl": 300,
"conditions": {
"maxExpiry": 30,
"maxCost": "0xB5E620F48000"
},
"provider": {
"name": "My App"
}
}
]
}payer_getTerms Example Return Value (Budgeted Sponsorship with Token Fallback)
The sender is enrolled in a subscription that grants free gas on the network. The payer sponsors and reports the remaining allowance (10 monthly budget), and also offers token payment as a fallback for when the budget is exhausted.
gasEstimate.gasLimit is sized for the worst-case offer: the user calls' 50,000 gas plus the 20,000-gas phase 0 (paymentGas). At 1.5 gwei that is 70,000 × 1.5 × 10^9 = 1.05 × 10^14 wei. The non-refundable rate 0x77359400 is 2,000 × 10^6, i.e. 2,000 USDC per native unit, so paymentAmount = mulDivUp(1.05 × 10^14, 2 × 10^9, 10^18) = 210,000 (0x33450, 0.21 USDC). The refundable choice's rate 0x9502F900 (2,500 USDC per native unit) gives 262,500 (0x40164, 0.2625 USDC) upfront with the unused portion returned; it routes to its own feeRecipient so the payer can tell the two apart at co-sign.
{
"gasEstimate": {
"gasLimit": "0x11170",
"maxFeePerGas": "0x59682F00",
"maxPriorityFeePerGas": "0x59682F00"
},
"options": [
{
"kind": "sponsored",
"payer": "0xCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCc",
"ttl": 300,
"conditions": {
"maxExpiry": 30
},
"provider": {
"name": "Coinbase One"
},
"balance": {
"kind": "sponsorship",
"limits": [
{
"unit": "asset",
"available": "0x4C4B40",
"limit": "0x989680",
"spent": "0x4C4B40",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"symbol": "USDC",
"decimals": 6
}
],
"validFor": 2592000
}
},
{
"kind": "token",
"payer": "0xCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCc",
"ttl": 60,
"conditions": {
"maxExpiry": 30
},
"tokens": [
{
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"symbol": "USDC",
"decimals": 6,
"rate": "0x77359400",
"paymentAmount": "0x33450",
"paymentGas": "0x4E20",
"feeRecipient": "0xFee5deE5deE5deE5deE5deE5deE5deE5deE5deE5"
},
{
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"symbol": "USDC",
"decimals": 6,
"rate": "0x9502F900",
"paymentAmount": "0x40164",
"paymentGas": "0x4E20",
"feeRecipient": "0xFee6dEe6dEe6dEe6dEe6dEe6dEe6dEe6dEe6dEe6",
"refund": {
"window": 86400
}
}
]
}
]
}payer_getTerms Example Return Value (Multiple Declines, Token Fallback)
This aggregated response queried two would-be sponsors and both declined for different reasons: the sender's Coinbase One monthly budget is exhausted (resets in three days), and the app's policy doesn't cover this contract. Each decline is its own sponsored_declined entry, so the wallet can explain both — "Coinbase One: free gas used up, resets in 3 days" and "My App: this action isn't covered" — and still offer the USDC token fallback that did come back.
{
"gasEstimate": {
"gasLimit": "0x11170",
"maxFeePerGas": "0x59682F00",
"maxPriorityFeePerGas": "0x59682F00"
},
"options": [
{
"kind": "sponsored_declined",
"code": "BUDGET_EXHAUSTED",
"reason": "Monthly free-gas allowance used up; resets in 3 days.",
"provider": {
"name": "Coinbase One"
},
"balance": {
"kind": "sponsorship",
"limits": [
{
"unit": "asset",
"available": "0x0",
"limit": "0x989680",
"spent": "0x989680",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"symbol": "USDC",
"decimals": 6
}
],
"validFor": 259200
}
},
{
"kind": "sponsored_declined",
"code": "POLICY_REJECTED",
"reason": "This contract isn't covered by the app's sponsorship policy.",
"provider": {
"name": "My App"
}
},
{
"kind": "token",
"payer": "0xCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCc",
"ttl": 60,
"conditions": {
"maxExpiry": 30
},
"tokens": [
{
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"symbol": "USDC",
"decimals": 6,
"rate": "0x77359400",
"paymentAmount": "0x33450",
"paymentGas": "0x4E20",
"feeRecipient": "0xFee5deE5deE5deE5deE5deE5deE5deE5deE5deE5"
}
]
}
]
}payer_getTerms Example Return Value (Sponsorship Declined: Gas Cap)
Gas prices have spiked to 3.5 gwei. The payer's per-transaction ceiling is 0xB5E620F48000 wei (2 × 10^14 wei, 0.49), so the payer declines sponsorship with the gas diagnostic. The wallet can show the gap and let the user wait or pay with the token offered alongside, which costs 490,000 (0x77A10, 0.49 USDC) at the same rate.
{
"gasEstimate": {
"gasLimit": "0x11170",
"maxFeePerGas": "0xD09DC300",
"maxPriorityFeePerGas": "0x59682F00"
},
"options": [
{
"kind": "sponsored_declined",
"code": "GAS_EXCEEDS_LIMIT",
"reason": "Gas above sponsorship cap. We cover up to $0.40 per transaction; this one would cost $0.49 at current prices.",
"gas": {
"estimatedCost": "0xDED381F85000",
"maxCost": "0xB5E620F48000"
},
"provider": {
"name": "My App"
}
},
{
"kind": "token",
"payer": "0xCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCc",
"ttl": 60,
"conditions": {
"maxExpiry": 30
},
"tokens": [
{
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"symbol": "USDC",
"decimals": 6,
"rate": "0x77359400",
"paymentAmount": "0x77A10",
"paymentGas": "0x4E20",
"feeRecipient": "0xFee5deE5deE5deE5deE5deE5deE5deE5deE5deE5"
}
]
}
]
}payer_sendTransaction
Accepts a sender-signed EIP-8130 transaction, co-signs it, submits it onchain, and returns the transaction hash. The transaction MUST have the payer field set to the payer's address and payer_auth left empty; the payer service fills in payer_auth with authenticator || data after validating the transaction. Payers MUST NOT mutate any sender-signed fields. If the payer cannot accept the transaction as-is (e.g., gas limit too low for the calls to execute → GAS_TOO_LOW with a suggested minGasLimit), it MUST reject and SHOULD return actionable error data.
payer_sendTransaction RPC Specification
type SendTransactionParams = {
signedTransaction: `0x${string}`;
context?: Record<string, any>;
};
type SendTransactionResult = {
transactionHash: `0x${string}`;
tokenCharged?: {
token: `0x${string}`;
amount: `0x${string}`; // gross phase-0 credit actually made to the recipient, known immediately
estimatedRefund?: `0x${string}`; // payer's estimate of the refund to settle later, when a `refund` policy applies
};
};tokenCharged reports the phase-0 credit the transaction makes, known without waiting for inclusion. When the choice carries a refund, estimatedRefund is the payer's estimate of what will be returned to from; the refund settles separately.
payer_sendTransaction Example Parameters
{
"signedTransaction": "0x...",
"context": {
"appId": "my-app"
}
}payer_sendTransaction Example Return Value
{
"transactionHash": "0x1234abcd..."
}payer_sendTransaction Example Return Value (Token Payment)
{
"transactionHash": "0x1234abcd...",
"tokenCharged": {
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "0x33450"
}
}payer_signTransaction
Accepts a sender-signed EIP-8130 transaction, co-signs it, and returns the fully signed transaction without submitting it; the wallet submits. The transaction format and constraints are the same as for payer_sendTransaction. Called against an offer that does not list it in methods, it returns JSON-RPC -32601 (Method not found).
payer_signTransaction RPC Specification
type SignTransactionParams = {
signedTransaction: `0x${string}`;
context?: Record<string, any>;
};
type SignTransactionResult = {
signedTransaction: `0x${string}`;
tokenCharged?: {
token: `0x${string}`;
amount: `0x${string}`; // gross phase-0 credit; see `payer_sendTransaction`
estimatedRefund?: `0x${string}`;
};
};Parameters are identical to payer_sendTransaction. Example return value:
{
"signedTransaction": "0x..."
}payer_getSponsorshipBalance (OPTIONAL)
Returns a sender's standing balances (budgeted sponsorship allowances and prepaid credit) held with the payer, without a transaction intent. Wallets use it to show users how much free gas or credit they have and to decide whether to attempt sponsorship or fall back to payment before assembling an intent.
It returns only intent-independent balances, such as a subscription's monthly free-gas allowance (sponsorship) or a prepaid credit, and MUST NOT try to enumerate per-intent sponsorship, which payer_getTerms evaluates. Once an intent exists, the applicable balance comes back on the matching sponsored offer. A payer with no standing balance for from returns an empty balances array.
When the endpoint is an aggregator fronting multiple services, each returned balance MUST carry source attribution (payer, endpoint, and SHOULD carry name) so the wallet can tell which service holds it. To target a single service, set payer (the service's address) or endpoint (its URL) in params; the aggregator returns only that service's balances. A payer MAY return multiple balances (e.g., a sponsorship allowance and a credit balance). When invoked through an app-provided paymasterService, the caller MAY pass the capability's context to scope the result to an app- or policy-specific allowance.
Discovery. This method runs before any offer exists, so it is not listed in methods. A wallet discovers it by calling it: a payer that does not implement it returns JSON-RPC -32601 (Method not found), and the wallet proceeds to payer_getTerms. Wallets SHOULD cache that outcome per endpoint.
payer_getSponsorshipBalance RPC Specification
type GetSponsorshipBalanceParams = {
from: `0x${string}`;
chainId?: `0x${string}`; // optional execution-chain filter
payer?: `0x${string}`; // aggregator: scope to a single service by address
endpoint?: string; // aggregator: scope to a single service by endpoint URL
kind?: ("sponsorship" | "credit")[]; // filter; e.g. ["credit"] to list only credit
context?: Record<string, any>; // opaque app context (e.g. policyId) to scope the balance
};
type GetSponsorshipBalanceResult = {
balances: PayerBalance[];
ttl: number; // how long this snapshot may be cached, in seconds
};payer_getSponsorshipBalance Example (Aggregator: Three Services)
Three services are fronted by the aggregator: a subscription with a monthly sponsorship allowance, a gas-market service where the user holds prepaid credit, and an app that grants a compound weekly allowance — $0.05 OR 3 transactions, whichever comes first.
{
"balances": [
{
"kind": "sponsorship",
"limits": [
{
"unit": "asset",
"available": "0x30D400",
"limit": "0x4C4B40",
"spent": "0x1B7740",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"symbol": "USDC",
"decimals": 6
}
],
"validFor": 2592000,
"payer": "0xCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCc",
"endpoint": "https://payer.coinbase.com/v1",
"name": "Coinbase One"
},
{
"kind": "credit",
"limits": [
{
"unit": "asset",
"available": "0xBEBC20",
"asset": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"symbol": "USDC",
"decimals": 6
}
],
"payer": "0xDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDd",
"endpoint": "https://gas.market/v1",
"name": "Gas Market"
},
{
"kind": "sponsorship",
"limits": [
{
"unit": "asset",
"available": "0xC350",
"limit": "0xC350",
"spent": "0x0",
"asset": "USD",
"decimals": 6
},
{
"unit": "count",
"available": "0x3",
"limit": "0x3",
"spent": "0x0"
}
],
"validFor": 604800,
"payer": "0xEeEeEeEeEeEeEeEeEeEeEeEeEeEeEeEeEeEeEeEe",
"endpoint": "https://app.example.com/payer/v1",
"name": "Weekly free gas ($0.05 or 3 tx)"
}
],
"ttl": 30
}The third entry illustrates the compound case: a $0.05 USD ceiling (0xC350 = 50000 in 6-decimal USD = 0.05, the grant declines (and a payer would surface that as BUDGET_EXHAUSTEDorSENDER_LIMIT_REACHED`).
Payer Error Codes
This spec uses one canonical vocabulary for payer-domain failures across both channels — sponsored_declined.code on a payer_getTerms response, and data.code on a JSON-RPC error from payer_sendTransaction / payer_signTransaction. The string is the canonical condition name; the numeric JSON-RPC code is a single envelope. Consumers MUST branch on the string and MAY ignore the numeric except for routing the envelope.
JSON-RPC envelopes used by payer methods:
-32000PAYER_REJECTED— every payer-domain rejection frompayer_sendTransaction/payer_signTransactionuses this envelope.datais aPayerRejectedData(below): the specific condition indata.code(string) plus any actionable detail: arequote(forPAYMENT_INSUFFICIENT), aminGasLimit(forGAS_TOO_LOW), arevert(forEXECUTION_REVERTED), ashortfall(forSENDER_BALANCE_INSUFFICIENT), agasdiagnostic, abalance, or freshoptionsto switch to.- Standard JSON-RPC codes (
-32600Invalid Request,-32601Method not found,-32602Invalid params,-32603Internal error,-32700Parse error) keep their protocol meanings and are returned for protocol-level faults, not for payer business logic.
payer_getTerms MUST NOT return a JSON-RPC error to signal "I cannot sponsor this intent." A query the payer evaluated successfully — even one that yields no offer — is a success response with an options array; sponsorship refusals ride on a sponsored_declined entry, and token absence is expressed by the absence of token offers. This keeps the optimistic single-call flow on the success path and gives the wallet structured, displayable reasons without an exception channel. payer_getTerms MAY still return standard JSON-RPC errors for genuine protocol faults, plus -32000 with data.code: "TEMPORARILY_UNAVAILABLE" if the payer cannot even evaluate the intent.
type PayerErrorCode =
// Decline scope: `sponsored_declined.code` and send/sign errors
| "POLICY_REJECTED"
| "SENDER_INELIGIBLE"
| "BUDGET_EXHAUSTED"
| "SENDER_LIMIT_REACHED"
| "GAS_EXCEEDS_LIMIT"
| "TEMPORARILY_UNAVAILABLE"
// Send/sign only: faults in a signed transaction
| "INVALID_TRANSACTION"
| "UNSUPPORTED_TOKEN"
| "PAYMENT_INSUFFICIENT"
| "GAS_TOO_LOW"
| "EXECUTION_REVERTED"
| "EXPIRY_OUT_OF_BOUNDS"
| "SENDER_BALANCE_INSUFFICIENT"
| "PAYER_BALANCE_INSUFFICIENT"
| string; // payers MAY return their own; wallets MUST treat unknown codes as opaque, displaying `reason` when present
// The only codes that may appear on `DeclinedSponsorship.code`
type SponsorshipDeclineCode =
| "POLICY_REJECTED"
| "SENDER_INELIGIBLE"
| "BUDGET_EXHAUSTED"
| "SENDER_LIMIT_REACHED"
| "GAS_EXCEEDS_LIMIT"
| "TEMPORARILY_UNAVAILABLE"
| string; // extensible, same opaque-unknown rule as PayerErrorCode
// The `data` payload carried on a `-32000` PAYER_REJECTED error from
// `payer_sendTransaction` / `payer_signTransaction`.
type PayerRejectedData = {
code: PayerErrorCode;
reason?: string; // human-readable detail for display/logs
balance?: PayerBalance; // for BUDGET_EXHAUSTED / SENDER_LIMIT_REACHED
gas?: { // for GAS_EXCEEDS_LIMIT
estimatedCost: `0x${string}`;
maxCost: `0x${string}`;
};
minGasLimit?: `0x${string}`; // for GAS_TOO_LOW: the smallest `gasLimit` the payer's simulation needs
requote?: { // for PAYMENT_INSUFFICIENT: the phase-0 credit the payer accepts now
token: `0x${string}`;
paymentAmount: `0x${string}`; // the minimum sufficient credit now, for this transaction's gas fields
feeRecipient?: `0x${string}`; // defaults to the offer's `payer`
rate?: `0x${string}`; // the current WAD rate, when it moved
ttl?: number; // relative seconds this corrected amount holds
};
revert?: { // for EXECUTION_REVERTED
phase: number; // 0 = payment transfer, 1 = user calls, ...
reason?: string; // decoded revert reason when available
data?: `0x${string}`; // raw revert return data
};
shortfall?: { // for SENDER_BALANCE_INSUFFICIENT
token: `0x${string}` | "native"; // the asset the sender is short of
required: `0x${string}`; // amount the transaction needs, in the asset's base units
available: `0x${string}`; // the sender's balance at simulation
};
options?: PaymentOption[]; // fresh offers for the same intent the wallet can switch to
fiatCurrency?: string; // REQUIRED when any offer in `options` carries a `fiatRate`
};The "Channel" column shows where each code MAY appear; the "Permanent?" column tells a wallet whether to retry the same intent against the same payer.
data.code / sponsored_declined.code | Channel | Permanent? | Description |
|---|---|---|---|
POLICY_REJECTED | declined + send/sign | Yes | The calls, contracts, or sender are outside this sponsor's policy for this intent. Pay with a token or use a different payer. |
SENDER_INELIGIBLE | declined + send/sign | Yes | Sender is rejected entirely (blocklisted, or missing a required attestation). |
BUDGET_EXHAUSTED | declined + send/sign | No | Sponsor budget or prepaid credit is depleted; balance.validFor indicates when it refills. |
SENDER_LIMIT_REACHED | declined + send/sign | No | Per-sender cap reached (tx count or USD); balance.validFor indicates when it refills. |
GAS_EXCEEDS_LIMIT | declined + send/sign | No | Current gas exceeds this payer's per-transaction ceiling. The payer SHOULD include the gas diagnostic. Retry when gas is lower. |
TEMPORARILY_UNAVAILABLE | declined + send/sign + getTerms error | No | Payer is degraded (e.g. rate feed down). Retry shortly. The only code that MAY be a payer_getTerms error. |
INVALID_TRANSACTION | send/sign | n/a | Malformed or invalid EIP-8130 transaction. |
UNSUPPORTED_TOKEN | send/sign | Yes | Token not accepted by this payer. |
PAYMENT_INSUFFICIENT | send/sign | No | Phase-0 credit is below the required amount at the payer's current rate. The payer SHOULD include a requote; see Re-Quote on PAYMENT_INSUFFICIENT. |
GAS_TOO_LOW | send/sign | No | gas_limit is too low for phase 0 or the user calls. The payer SHOULD include minGasLimit; the wallet raises gas_limit (within conditions.maxGasLimit), recomputes any token amount, and re-signs. |
EXECUTION_REVERTED | send/sign | No | The payer's simulation shows a call would revert, so it refuses to co-sign. The payer SHOULD include revert. Simulation is OPTIONAL; a payer that does not simulate lets reverts surface in the receipt. |
EXPIRY_OUT_OF_BOUNDS | send/sign | No | Transaction valid_before implies an expiry beyond the offer's conditions.maxExpiry. |
SENDER_BALANCE_INSUFFICIENT | send/sign | No | The sender cannot fund the transaction: its balance of the phase-0 token, or of the native asset a call transfers, is below what the transaction moves. The payer SHOULD include shortfall, and SHOULD use this code rather than EXECUTION_REVERTED when it identifies that cause. The sender funds the account, or the wallet switches to another offer. |
PAYER_BALANCE_INSUFFICIENT | send/sign | No | Payer lacks ETH to cover gas at the protocol level. |
Switching offers on rejection. When the rejected choice cannot succeed as built but the payer has other terms for the same intent, it SHOULD include those terms as options, shaped and constrained like the options of a payer_getTerms result (including the Co-Sign Disambiguation Invariant). For example, a sponsor rejecting with BUDGET_EXHAUSTED returns its token offer, and a payer rejecting with SENDER_BALANCE_INSUFFICIENT for USDC returns its offer with the tokens the sender does hold. The intent is the rejected transaction's sender and user calls, and each paymentAmount is priced at the rejected transaction's gas fields. The wallet treats options as a fresh quote: it prices and picks from them as in the flow, rebuilds, re-signs, and calls a co-sign method of the picked offer, without calling payer_getTerms again. requote remains the path for retrying the same choice at a corrected amount.
For example, a sender that picked USDC but holds only 0.012 USDC is rejected with a shortfall and offered cbETH instead:
{
"code": -32000,
"message": "PAYER_REJECTED",
"data": {
"code": "SENDER_BALANCE_INSUFFICIENT",
"reason": "Sender holds 0.012 USDC; this transaction pays 0.216 USDC.",
"shortfall": {
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"required": "0x34BC0",
"available": "0x2EE0"
},
"options": [
{
"kind": "token",
"payer": "0xBa5EBa5EBa5EBa5EBa5EBa5EBa5EBa5EBa5EBa5E",
"ttl": 60,
"conditions": { "maxExpiry": 30 },
"tokens": [
{
"token": "0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22",
"symbol": "cbETH",
"decimals": 18,
"rate": "0xC9DBD5D80E68BA2",
"paymentAmount": "0x594BBC6568BB",
"paymentGas": "0x61A8"
}
]
}
]
}
}Re-Quote on PAYMENT_INSUFFICIENT
Nothing onchain binds phase 0 to the quote, so the co-sign check is the payer's last line of defense: if the credit is below the required amount at the payer's current rate, the payer MUST reject with PAYMENT_INSUFFICIENT. An expired ttl is not itself a rejection; a stale credit that still meets the current requirement is accepted. Because the required amount scales with the transaction's own gas fields, gas-price movement does not cause this error; a rate that moved after ttl does. Wallets MUST treat it as an expected loop:
- Catch
PAYER_REJECTEDwithdata.code === "PAYMENT_INSUFFICIENT". - With a
requote, re-sign phase 0 creditingrequote.paymentAmounttorequote.feeRecipient(default: the offer'spayer) and go to step 4. - Without one, call
payer_getTermsagain, re-select the same(token, feeRecipient)choice if possible, recompute the amount from its freshrate, and re-sign. - Retry the co-sign method. Implementations SHOULD stop after one or two retries and surface the failure.
Wallets pricing from a cached rate near the end of its ttl MAY add a few percent of headroom to reduce retries.
A future version MAY define a signed-quote profile, in which the payer signs its rate and expiry so the quote can be verified without trusting an unsigned feed.
paymasterService Capability
This proposal reuses the paymasterService capability from ERC-7677 unchanged: apps pass a url and optional context on an EIP-5792 wallet_sendCalls (or ERC-7836 wallet_prepareCalls) request, exactly as specified there. For EIP-8130 transactions the wallet uses the payer_* methods defined here; for ERC-4337 user operations it uses the pm_* methods from ERC-7677. The wallet MUST forward the opaque context to payer_getTerms and to payer_sendTransaction / payer_signTransaction.
Wallets indicate support by returning the paymasterService capability as supported from EIP-5792 wallet_getCapabilities. Because the app-provided payer may not ultimately be used (service failure or user selection), apps MUST NOT assume it is the entity that pays.
wallet_sendCalls Flow
When wallet_sendCalls carries the paymasterService capability and the wallet is constructing an EIP-8130 transaction, the wallet follows the flow with the app's URL as one of its payer sources: it quotes, builds and signs the transaction with the picked offer's payer (see Transaction Construction), and calls one of that offer's co-sign methods, choosing between them when the offer lists both, or submits directly for an offer with payerAuth. The same methods apply to ERC-7836 wallet_prepareCalls / wallet_sendPreparedCalls, with the app signing the transaction digest between the two calls.
Wallet-Selected Payers
The paymasterService capability covers the case where an app directs the wallet to a service. It is not the only source of a payer. A wallet that wants its users to pay gas in a token, with no app involvement, selects a payer itself and uses the same payer_* methods.
Typical wallet-configured payers:
- a payer service the wallet operator runs or contracts with, configured per chain.
- the chain's own node or sequencer, when it serves
payer_*on its standard JSON-RPC endpoint (see Payer Service Interface).
When both an app-provided paymasterService and a wallet-default payer are available, the wallet decides which to use, for example preferring app sponsorship when offered and falling back to its default token-payment payer otherwise. Because sender_auth binds the chosen payer, the user always signs the exact payer that will be used.
Aggregation
A wallet may have several payer sources for one intent: for example a sequencer that accepts stablecoins, an app-provided paymasterService URL that may sponsor, and a subscription the user holds an allowance with. They combine in two ways, both using only the methods above.
Client-side fan-out. The wallet calls payer_getTerms on each source in parallel with its own eth_estimateGas and fee reads, so the round-trip count matches a self-paid transaction. It prices every token choice, and self-pay, from one set of gas numbers: gas_limit is its estimate for the user calls plus the phase-0 gas, and the payment is the required amount for that gas_limit. Wallets SHOULD rank offers on these wallet-computed figures rather than on each payer's gasEstimate or paymentGas, so a payer cannot look cheaper by understating gas (see Security Considerations). Because sender_auth binds the chosen payer, the wallet commits to exactly one payer when it signs.
Aggregator endpoint. A single service MAY front several downstream payers behind one URL and implement the same methods. Because each offer carries its own payer, endpoint, and methods, the aggregator MAY return one chosen offer or several offers from different downstreams, each pointing at the endpoint that co-signs it. Its payer_getSponsorshipBalance returns one attributed PayerBalance per service. When several downstream sponsors decline, it SHOULD return one sponsored_declined entry per sponsor, each with its own provider, so the wallet can explain each. It MUST keep co-sign fingerprints unique across downstreams per the Co-Sign Disambiguation Invariant.
Offers are not totally ordered: a lower rate without a refund can cost more than a higher one with a refund, and refund settlement, window, and payer reputation also matter. An aggregator that returns a single offer MUST rank by a documented policy (for example "cheapest guaranteed" or "lowest expected cost assuming the refund settles") rather than implying a canonical best. Returning several offers leaves that choice to the wallet and user.
Transaction Construction
The EIP-8130 AA transaction wire format is:
AA_TX_TYPE || rlp([
chain_id, sender, nonce_key, nonce_sequence, valid_after, valid_before,
max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
account_changes, calls, metadata,
payer, sender_auth, payer_auth
])valid_after and valid_before are uint64 Unix timestamps (0 means unbounded); the transaction type accepts seconds or milliseconds and normalizes to milliseconds. This specification's expiry and conditions.maxExpiry are relative seconds, which the wallet maps onto valid_before; the payer bounds valid_before with maxExpiry. metadata is opaque bytes carried by the transaction; this specification does not populate it, but it is covered by sender_auth.
calls is a two-level array of phases: [[call, ...], [call, ...]] where each call is [to, data]. Phases execute in order; each phase is atomic (all-or-nothing), but completed phases persist independently.
Note on value: EIP-8130 calls carry no protocol value; each onchain call is [to, data] with msg.value == 0. The wallet realizes any non-zero intent value through the account's wallet bytecode (e.g., an executeBatch call that performs the value-bearing CALL). An intent call whose value is 0 or omitted is encoded directly as [to, data].
Constructing the Transaction
Given terms from payer_getTerms, the wallet first picks one selectable PaymentOption from options; if the picked offer is a kind: "token" offer, the wallet additionally picks one TokenChoice from its tokens. The transaction is then built entirely from that offer (and, for token payment, that TokenChoice):
- Set
payerto the selected offer'spayer. - Set
nonce_keyandnonce_sequence: select anonce_key(a parallel-nonce lane;0for sequential ordering) and read the nextnonce_sequencefor(sender, nonce_key). Note thateth_getTransactionCounthas nononce_keyparameter and so only returns the sequence for the default lane (nonce_key == 0); reading any other lane requires an EIP-8130-specific accessor for(sender, nonce_key), which wallets using parallel lanes MUST use instead. - Set the validity window. Set
valid_after = 0: many mempools drop or reject a transaction that is not yet active, which would waste the payer's co-signature. Setvalid_before = now + expiry, where RECOMMENDEDexpiry = min(conditions.maxExpiry, max(15s, 3 × block_time)): about 15 seconds on fast L2s and 36 seconds on a 12-second-block L1. Three block times survives one missed block, and the floor keeps fast chains responsive; a window that is too tight costs a full re-sign and resubmit. The payer rejects avalid_beforebeyondnow + maxExpirywithEXPIRY_OUT_OF_BOUNDS. - Set gas parameters (
max_fee_per_gas,max_priority_fee_per_gas,gas_limit), typically from the wallet's own estimation run in parallel withpayer_getTerms. For atokenoffer,gas_limitis the user calls' estimate plus the phase-0 gas (paymentGasfor the default transfer). A wallet MAY instead usegasEstimateas-is. Either way, before signing the wallet MUST ensuregas_limitcovers its own estimate or simulation of the complete transaction, phase 0 included, and MUST respect the offer'sconditions. - Construct
callsusing call phases:
Offer kind | Phase 0 | Phase 1 |
|---|---|---|
sponsored | (none) | [user_calls] |
token | [token.transfer(choice.feeRecipient ?? offer.payer, amount)] | [user_calls] |
amount is the required amount for the transaction's own gas fields, which equals choice.paymentAmount when the wallet uses gasEstimate as-is. The phase-0 row shows the default "transfer" construction; under paymentMode: "any" other constructions are allowed. A sponsored offer, with or without a balance, has no phase 0, and the wallet MAY send a single phase [[user_calls]]; a balance-funded sponsor draws on the sender's budget off-chain when it co-signs.
- Set
payer_auth: leave it empty for the payer to fill in at co-sign, or, for an offer withpayerAuth, set it to exactly those bytes. - Sign: compute the sender signature hash over every field except
sender_authandpayer_auth, and producesender_auth.
Token Payment via Call Phases
EIP-8130 calls carry no ETH value; token movements are executed through contract calls. What 8168 pins is the outcome, not the call shape: phase 0 is valid when, at its end, recipient (the selected TokenChoice's feeRecipient when present, otherwise the offer's payer) has been credited at least the required amount of choice.token. Phase 0 commits independently, so the recipient keeps that credit regardless of whether the user's calls in phase 1 succeed or revert; this is how the payer is compensated.
Required amount. The required amount is computed from the transaction's own gas fields and the choice's rate:
required = ceil(gas_limit × max_fee_per_gas × choice.rate / 10^18)Implementations MUST compute it with exact integer arithmetic, multiplying before dividing and rounding up once (equivalent to Solidity mulDivUp(gas_limit * max_fee_per_gas, rate, 1e18)), so the wallet and payer derive the same value. rate is a WAD: token atomic units per 10^18 wei. For example, USDC (6 decimals) at 2,000 USDC per native unit is rate = 2,000 × 10^6 = 0x77359400. Truncating rate to whole atomic units changes the required amount by less than one atomic unit for any transaction costing under one native unit.
Default construction (paymentMode: "transfer"). The wallet builds a single phase-0 call that pushes the tokens from the sender to the recipient:
offer = terms.options[i] // the `kind: "token"` offer the wallet picked
choice = offer.tokens[j] // the `TokenChoice` the wallet picked
recipient = choice.feeRecipient ?? offer.payer
amount = mulDivUp(gas_limit * max_fee_per_gas, choice.rate, 1e18) // the required amount
phase_0 = [
[choice.token, abi.encodeCall(IERC20.transfer, (recipient, amount))]
]This uses IERC20.transfer (a push from the sender) rather than transferFrom, so no token approvals are required, and the payer verifies the payment by inspecting the call — no simulation. This is a significant simplification over ERC-4337 paymaster flows, which require the sender to pre-approve the paymaster contract (or inject an approve() call into the UserOperation's calldata) before the paymaster can pull payment in postOp. It is the RECOMMENDED construction and the only one a "transfer"-mode payer accepts.
Richer constructions (paymentMode: "any"). A payer MAY instead advertise that it accepts any phase-0 calls that produce the same credit — for example a transferFrom from a funding account, a session-key module call, a privacy-pool withdrawal that splits proceeds to the recipient and the sender, or a non-standard token's transfer method. Phase 0 is a multi-call array, so several calls MAY compose the payment; only the net credit to recipient is constrained (it MUST be at least the required amount), and the remaining movement is unconstrained. These constructions may re-introduce approvals or funding dependencies and can fail at inclusion in ways a self-push cannot; that is the wallet's and payer's tradeoff to accept.
Payer verification is policy. In either mode the payer's job reduces to the same pipeline — confirm the phase-0 balance outcome, apply its own risk scoring, then co-sign (or reject). A "transfer"-mode payer confirms the outcome by inspecting the transfer call; an "any"-mode payer confirms it by simulating/tracing phase 0. How it risk-scores (sender reputation, token/velocity limits, funding-source checks) is entirely up to the payer service; 8168 dictates only that the credit land.
Payer Co-Signing
The payer receives the sender-signed transaction (with payer_auth empty), validates it against its terms, computes payer_signature_hash using AA_PAYER_TYPE (see EIP-8130 Signature Payload), and fills in payer_auth with authenticator || data. The payer's actor MUST have PAYER scope in its actor_config on the payer account.
A payer service MAY delegate co-signing to a separate signing service that custodies the PAYER-scoped key, for example to separate pricing and policy logic from key management and submission. The payer field always identifies the account whose actor signs, so sender_auth binds the transaction to that account regardless of which operator runs the endpoint.
Preauthorized Payers
A payer whose authenticator validates payment onchain, for example a verifier contract that checks the phase-0 credit against its own price feed, does not need to see each transaction. Its offers carry payerAuth: payer_auth bytes that are valid for every transaction meeting the authenticator's rules. The wallet builds the transaction from the offer as usual, sets payer_auth to payerAuth, and submits it through the chain's ordinary transaction path (eth_sendRawTransaction); it calls no co-sign method unless the offer lists one. payerAuth works with either kind: a token offer's authenticator requires the phase-0 credit, and a sponsored offer's authenticator gates on onchain state such as an allowlist or per-account quota.
- Validity is onchain. No payer service checks the transaction, so the authenticator's rules are the only terms that bind. For a
tokenoffer,rateis the payer's quote of what those rules accept whilettlholds. The wallet MUST simulate the complete transaction withpayerAuthin place before submitting, and SHOULD add headroom when the authenticator reprices from a live feed. - The bytes are public. Anyone can attach
payerAuthto any transaction naming thispayer. A payer MUST NOT publishpayerAuthunless its authenticator enforces payment and policy onchain; a signature over a single transaction's payer hash cannot be preauthorized. - Static bytes only.
payerAuthsuits authenticators whosedatais constant or empty. An authenticator that needs data specific to each transaction uses co-signing instead. - Chain policy applies. The payer account needs an actor for this authenticator with PAYER scope, and nodes MAY refuse authenticators outside their canonical set (see EIP-8130). A payer SHOULD publish
payerAuthonly on chains that accept its authenticator. - Quotes carry a buffer. An authenticator that prices from shared state, such as a price feed, can reject a pooled transaction after that state moves. A payer SHOULD quote
ratewith enough headroom that ordinary movement within the feed's staleness window does not invalidate a transaction built at the quote.
For example, a USDC offer whose verifier at 0xEeEe…EeEe ignores data publishes the verifier's address as payerAuth:
{
"kind": "token",
"payer": "0xDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDd",
"payerAuth": "0xEeEeEeEeEeEeEeEeEeEeEeEeEeEeEeEeEeEeEeEe",
"ttl": 30,
"conditions": { "maxGasLimit": "0x7A120", "maxCost": "0xB5E620F48000" },
"tokens": [
{
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"symbol": "USDC",
"decimals": 6,
"rate": "0x77359400",
"paymentAmount": "0x33450",
"paymentGas": "0x4E20"
}
]
}The absent methods means the offer has no co-sign method. The wallet sets payer and payer_auth, credits the payer the required amount in phase 0, signs, simulates, and broadcasts.
Token Cost Transparency
The token cost is fixed before the sender signs, so wallets can display it exactly: the phase-0 amount, rendered with the choice's decimals and symbol. rate reads directly as a price (0x77359400 with 6 decimals is 2,000 USDC per native unit). With fiatRate, the approximate fiat cost is amount × fiatRate / (10^decimals × 10^6); in the running example, 0.21 USDC at 0xF4240 (1.00 USD) is $0.21. Without a refund this is the final charge; with one it is an upper bound.
Endpoint Versioning
Payer service endpoints MUST use path-based major versioning (e.g., /v1, /v2). Method names remain stable across versions. Breaking changes (new required fields, changed semantics) require a new major path version. Additive optional fields in request or response are backwards-compatible within the same version. Clients MUST ignore unknown fields. Payers MUST NOT require new optional fields within an existing version.
Rationale
Why New RPC Methods
While the paymasterService capability is shared, the RPC methods must differ. ERC-7677's two-phase pm_getPaymasterStubData / pm_getPaymasterData flow exists because ERC-4337 paymasters inject data into the UserOperation before gas estimation, and the stub data must produce consistent code paths and byte lengths. In EIP-8130, the payer simply co-signs the complete transaction; there is no paymaster data injection, no stub values, and no code path consistency concern. The payer_* methods reflect this simpler model. Service providers that support both wallet types SHOULD implement both method sets at the same endpoint URL.
payer_sendTransaction vs payer_signTransaction
Two methods mirroring the eth_sendTransaction / eth_signTransaction convention:
payer_sendTransactionminimizes hops: the payer co-signs and submits in one step. This is the common case for app-sponsored transactions.payer_signTransactiongives the wallet submission control, useful for high-value transactions, MEV-sensitive paths, or when the wallet wants to use private mempools.
They are kept as two methods rather than one method with a submit/relay flag because their return types genuinely diverge — a transactionHash versus the co-signed transaction bytes — and a merged method would only relocate the "is wallet-side submission available?" question from which method exists to which parameter value is supported, turning a statically-knowable fact into a runtime error-and-fallback. Two methods let the wallet learn the path up front from the picked offer's methods field and never make a call it knows will fail.
Requiring co-signing rather than a specific transport lets each payer expose the paths its infrastructure supports:
- Submit-only. Most payers relay, so an offer that omits
methodssupportspayer_sendTransaction. A payer that co-signs just-in-time during block building has no bytes to return before inclusion. Returning bytes also opens a window in which the sender's token balance can move or the bytes can reach a competing builder; relay-only closes it. - Sign-only. A pure co-signer without relay infrastructure, or a deployment where the wallet always submits, lists only
payer_signTransaction, and the wallet submits the returned bytes.
A payer listing both leaves the choice to the wallet, not the app.
feeRecipient Placement
feeRecipient is per token and per intent rather than per payer: a payer may route USDC to a stablecoin treasury and another token to an operator wallet, route fees by app or policy, or split revenue per integration. Carrying it on each TokenChoice in the quote gives the wallet everything it needs to build phase 0, including from cached terms, without a separate discovery step.
The recipient also identifies the choice at co-sign. Because the fingerprint is the credited (token, recipient) and not the amount, a wallet can price the payment from its own gas numbers and the payer still knows which terms apply.
Why Preauthorized Payers
The co-sign methods assume a live payer that signs, and so can decline, each transaction. EIP-8130 also admits payers that are not live: payer_auth is validated by an authenticator the payer nominates, and that authenticator MAY be a stateful predicate. It reads the pending transaction through the transaction-context precompile (sender, payer, gas limit, maximum cost, call phases), reads whatever state it prices against, and succeeds or reverts. A price-feed token verifier is the canonical case: it requires phase 0 to credit the payer at least the maximum gas cost converted at an onchain feed rate, optionally gated by a blocklist.
Such a payer is permissionless and veto-free. The transaction is complete once the sender signs, validity is a function of state any node can check, and no party can withhold a co-signature, so the transaction is eligible for fork-choice enforced inclusion lists like any other. A just-in-time co-signer cannot offer this, since it holds an inclusion veto.
8168's role for these payers narrows to discovery and quoting. Everything the wallet needs, including the verifier and its parameters, is onchain, so a wallet MAY build the transaction with no payer service at all by recomputing the verifier's own math. payer_getTerms remains useful as the convenience and aggregation layer, which is why payerAuth is a field on an ordinary offer rather than a separate interface. The profile depends on the transaction-context precompile exposing the fields a payer predicate needs, which is an EIP-8130 concern.
Sequencer-Operated Payer Implementations
The wallet protocol is identical for every payer, but a payer operated by the chain's sequencer (or by a party with privileged inclusion) has operational levers worth noting as one possible implementation pattern; this is non-normative and many variations are possible:
- Drop-on-failure: because the operator builds the block, it MAY simulate phase 0 against pending state and omit the transaction entirely if phase 0 would revert, avoiding nonce consumption on a failed payment transfer.
- Exclusive inclusion: listing only
payer_sendTransactionmeans the wallet never receives the co-signed bytes, so no third party can broadcast them to a competing builder or the public mempool. - Pattern-aware policies: a sequencer-payer MAY apply broader heuristics, for example dropping transactions whose phase 0 fails in patterns consistent with probing or griefing against a particular token set.
These are operator choices, not wire-protocol additions; to a wallet, a sequencer-operated payer is just a payer.
Security Considerations
API key exposure: Payer service URLs commonly contain API keys. App developers SHOULD proxy payer requests through their own backend to keep keys secret, identical to the recommendation in ERC-7677. Payer services SHOULD support scoped API keys restricted to specific methods.
Relay trust: With payer_sendTransaction, the wallet trusts the payer to submit. valid_before bounds this exposure: a transaction not included by then is dead. Wallets concerned about relay trust SHOULD prefer offers that list payer_signTransaction and submit themselves.
Rate commitment: A payer SHOULD accept, until the offer's ttl, any phase-0 credit that meets the required amount at the quoted rate. Because the required amount scales with gas_limit × max_fee_per_gas, the payer is compensated for whatever gas ceiling the wallet sets, so fee movement alone does not invalidate a quote; only a rate change does.
Understated gas: Phase 0 commits independently of the user's calls, so a transaction whose gas_limit covers phase 0 but not the calls pays the payer while the calls revert out of gas. A payer that understates paymentGas or gasEstimate can cause exactly this, and in a multi-payer comparison an understated figure also makes its offer look cheaper. Wallets MUST NOT rely on either value for correctness: before signing, the wallet MUST ensure gas_limit covers its own estimate or simulation of the complete transaction, phase 0 included, and SHOULD rank offers on its own gas figures. Under the default "transfer" mode, phase 0 is a canonical IERC20.transfer, so this check costs no extra round trip. The mirror case, an overstated paymentGas that inflates a non-refundable charge, is handled the same way. An honest payer protects itself by simulating at co-sign and rejecting with GAS_TOO_LOW and minGasLimit.
Preauthorized payers: A published payerAuth authorizes every transaction its authenticator accepts, from any sender. Its safety rests entirely on that authenticator enforcing payment and policy onchain, so payers MUST treat publishing it as deploying an open sponsorship, and wallets MUST simulate before submitting (see Preauthorized Payers). An authenticator that validates against shared mutable state, such as a price feed or blocklist, can flip a pooled or force-included transaction's validity when that state changes; the quote buffer is the mitigation. A sender that moves its tokens after submission only invalidates its own transaction. With no service in the loop there is no refund path, so wallets SHOULD present these offers as fixed-price.
Balances are advisory: A returned balance MUST NOT be treated as a spend guarantee. Wallets MUST handle BUDGET_EXHAUSTED and other rejections at co-sign time, since concurrent transactions or policy may invalidate a stale balance snapshot. Payers MUST enforce the actual budget server-side and MUST NOT rely on the wallet honoring the advertised balance.
Phase-0 settlement risk (token payment): Between payer_getTerms and inclusion, a sender's token balance can change (front-running, concurrent transfers, or intentional griefing) and leave the payer with a reverted phase-0 transfer and no compensation. Payers SHOULD put mitigations in place appropriate to their risk tolerance, for example sender risk scoring, allowlists for trusted senders or tokens, deposit-history requirements, and velocity limits. Builder-operated payers MAY additionally drop phase-0 failures pre-inclusion (see Sequencer-Operated Payer Implementations). A payer that advertises paymentMode: "any" widens this surface: it MUST verify the net credit by simulation/trace rather than inspection (a call labeled like a transfer is not trusted to net the amount), and richer constructions — transferFrom, pool withdrawals, session-key calls — have more ways to fail or be unwound between co-sign and inclusion, so such payers SHOULD weight their risk scoring accordingly. Payers with low risk tolerance SHOULD stay on the default "transfer" mode, which is verifiable by inspection.
Refund settlement is trust-based: A RefundPolicy on a TokenChoice commits the payer to return the phase-0 credit minus actual gas cost to from as a separate transfer within window seconds — but this spec has no onchain enforcement. The payer transfers from its own balance, paying the gas itself, and a sender has no programmatic recourse if the refund does not arrive. tokenCharged.estimatedRefund is the payer's estimate, not a binding receipt. Wallets MUST display refund amounts as expected rather than guaranteed (e.g. "up to X refunded"), SHOULD surface the payer's identity and refund window so the user can weigh the trust assumption, and SHOULD treat a missed refund as a signal that lowers their ranking of that payer for future intents. Payers SHOULD make refund settlements externally observable (an indexable event from a known address or settlement contract) so refund streams are auditable. Onchain refund settlement (e.g. an escrow contract that holds the phase-0 credit and releases overage to from permissionlessly) is a meaningful extension and a candidate for a future profile of this spec.
Copyright
Copyright and related rights waived via CC0.
