EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8168: Payer Service Capability

How apps, wallets, and payer services arrange gas payment for EIP-8430 and EIP-8141 transactions

Authors
Created2026-02-19
Discussion Linkhttps://ethereum-magicians.org/t/erc-8168-payer-services-for-erc-8130/28762
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1555PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationPayer Service InterfacepayergetTermspayersendTransactionpayersignTransactionpayergetSponsorshipBalance (OPTIONAL)Transaction ConstructionAA TransactionsFrame TransactionsToken PaymentToken Cost TransparencyPayer AuthorizationOpen Payer OffersPreauthorized PayersPayer Error CodesRe-Quote on PAYMENTINSUFFICIENTPayer SourcespaymasterService CapabilityWallet-Selected PayersAggregationKeystore IntegrationEndpoint VersioningRationaleWhy New RPC MethodspayersendTransaction vs payersignTransactionfeeRecipient PlacementWhy Preauthorized PayersConditional Inclusion for Submitting PayersSecurity ConsiderationsCopyright

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 native account abstraction by defining payer_* RPC methods that wallets use when constructing EIP-8430 AA transactions or EIP-8141 frame 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 and wallet-configured payers, 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-8430 introduces native account abstraction where payers are first-class protocol participants that co-sign the transaction with their own authorization, separate from the sender's. 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). EIP-8141 frame transactions reach the same model differently: the payer approves payment in its own frame and signs an entry of the transaction's signatures.

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 fit neither transaction type, 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, sponsorship drawn from a standing balance (e.g. a subscription allowance or prepaid credit), and payment for gas in a token. It works on any chain with the EIP-8430 transaction type, with plain secp256k1 accounts, and extends to the EIP-8130 Keystore where it is integrated. The same methods serve EIP-8141 frame transactions; the shape of the intent tells the payer which type the wallet is building.

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:

MethodSupportPurpose
payer_getTermsREQUIREDQuote payment options for a transaction intent, before the sender signs
payer_sendTransactionPer offerCo-sign a sender-signed transaction and submit it
payer_signTransactionPer offerCo-sign a transaction, signed by the sender or not yet, and return payerAuth
payer_getSponsorshipBalanceOPTIONALRead 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"], unless the offer is preauthorized, in which case it means none. A wallet MUST NOT call a co-sign method that the offer it is using does not list.

The payer_* prefix names the responder's role, not a kind of server. An app's paymasterService URL and a wallet-configured payer serve the same methods with the same response shapes, so a wallet has a single integration.

Flow.

  1. Quote. In parallel, the wallet calls payer_getTerms on each payer source it has and reads gas and fees from the chain (eth_estimateGas and fee fields), as it would for a self-paid transaction.
  2. Choose. The wallet prices every option from its own gas numbers (see Transaction Construction), and the user or wallet picks one.
  3. Authorize. The wallet builds the transaction from the offer, collects the sender's signature, and obtains the payer's authorization from a co-sign method or from the offer itself (see Payer Authorization).
  4. Submit. The wallet submits the transaction, unless it used payer_sendTransaction.

A caller that already knows an offer's terms, for example under a fixed agreement with the payer, MAY skip the quote. 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}`;
  // Exactly one of `calls` or `frames`, which selects the transaction type (see Transaction Construction)
  calls?: { // an EIP-8430 AA transaction's user calls
    to: `0x${string}`;
    value?: `0x${string}`;
    data?: `0x${string}`;
  }[];
  frames?: Frame[]; // an EIP-8141 frame transaction's wallet frames: any `deploy` frame, the sender's one `VERIFY` frame, then the user's `SENDER` frames
  gasLimit?: `0x${string}`; // OPTIONAL, `calls` only: wallet's gas for the transaction without phase 0 (its estimate for the user calls). The payer adds phase-0 gas itself
  // AA 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, Keystore integration only: bytes32 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
  // Frame transaction inputs
  signatures?: FrameSignature[]; // OPTIONAL, `frames` only: placeholders for the sender's `signatures` entries, priced by length and never verified
  preferredTokens?: (`0x${string}` | "native")[]; // 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
    gasLimit?: `0x${string}`; // `calls` requests: phase-0 gas included
    frameLimits?: Limits[]; // `frames` requests: one per request frame, in order
    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 | TokenOffer;

type BaseOffer = {
  payer: `0x${string}`; // the paying account: the AA transaction's `payer` ("0x00" for an open offer), or the frame transaction's `pay` frame target
  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 for a preauthorized offer
  payerAuth?: `0x${string}`; // the payer's authorization bytes, placed in the transaction as-is; opaque to the wallet. See Preauthorized Payers
  frames?: PayerFrames; // REQUIRED on offers for `frames` requests, absent otherwise: the payer's frames and signature entry
  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 `max_gas`
    maxCost?: `0x${string}`; // hard cap on the transaction's `max_cost`, in wei
  };
  provider?: Provider;
};

type Provider = { // display info for the party behind an offer or balance
  name: string;
  icon?: string; // data URI
};

type Frame = { // an EIP-8141 frame, with quantities as hex
  mode: `0x${string}`; // "0x0" DEFAULT, "0x1" VERIFY, "0x2" SENDER
  flags: `0x${string}`;
  target: `0x${string}` | null; // null means the sender
  limits?: Limits; // in requests, the wallet's estimate; absent means the payer estimates it
  value?: `0x${string}`;
  data?: `0x${string}`;
};

type Limits = {
  execution: `0x${string}`;
  state: `0x${string}`;
};

type FrameSignature = {
  scheme: `0x${string}`; // "0x0" ARBITRARY, "0x1" SECP256K1, "0x2" P256
  signer?: `0x${string}`;
  msg?: `0x${string}`;
  signature: `0x${string}`;
};

type PayerFrames = {
  pay: { // the VERIFY frame targeting `payer` with flags APPROVE_PAYMENT
    data: `0x${string}`;
    limits: Limits;
  };
  signature?: { // the payer's entry at `signatures[1]`, with an empty `msg`; absent when `pay` needs none
    scheme: `0x${string}`; // "0x1" or "0x2"; "0x0" only with `payerAuth`
    signer?: `0x${string}`;
  };
  postOp?: { // a DEFAULT frame appended after the user's frames
    target: `0x${string}`;
    data: `0x${string}`;
    limits: Limits;
  };
};

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
  cost?: { // present only for `COST_EXCEEDS_LIMIT`
    estimated: `0x${string}`; // `max_cost` in wei the payer estimated for this intent at current gas prices
    limit: `0x${string}`; // per-transaction ceiling in wei
  };
  provider?: Provider; // the would-be sponsor
};

type TokenOffer = 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}` | "native"; // ERC-20 address, or "native" for payment by value transfer
  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)
  paymentAmount?: `0x${string}`; // the required amount at the response's `gasEstimate`; REQUIRED when `gasEstimate` is present, omitted otherwise
  paymentGas?: `0x${string}`; // advisory, `calls` requests: gas the default phase-0 transfer adds to `gas_limit`. Payers SHOULD include it
  paymentLimits?: Limits; // advisory, `frames` requests: limits for the default payment frame. 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`, as a WAD (e.g. "0xDE0B6B3A7640000" = 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 service's payer address
  endpoint?: string; // the service's endpoint URL
  provider?: Provider; // the service's display info
};

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
FieldNotes
optionsPayment 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_declinedTells 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. cost is present only for COST_EXCEEDS_LIMIT: the payer's per-transaction ceiling and its cost estimate for this intent, so the wallet can show the gap.
payer / endpointpayer is the paying account: on an AA transaction the value of its payer field, which is 0x00 for an open offer, and on a frame transaction the pay frame's target. A named payer is also 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.
methodsThe 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.
payerAuthAuthorization bytes the payer provides for any transaction that satisfies the offer: payer_auth on an AA transaction, or the payer's signature entry bytes on a frame transaction. They are opaque: the wallet places exactly these bytes, signs, and submits without inspecting them. See Preauthorized Payers.
framesThe payer's part of a frame transaction, which the wallet places unmodified (see Frame Transactions): the pay frame's data and limits, the scheme and signer of the payer's signature entry, and an optional postOp frame. The sender signs over all of it, so it is fixed in the offer rather than added at co-sign. Its limits count toward max_gas, and so toward a token offer's required amount. A wallet MUST skip an offer whose frames carries a field it does not recognize: a later revision MAY add fields that change where or how the payer's frames are placed, and a wallet that ignored one would build a transaction the payer rejects.
ttlHow 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.
gasEstimateAdvisory gas values shared by every offer in the response: for calls requests a gasLimit sized for the worst case (phase-0 gas included), and for frames requests frameLimits for the request's frames, since the payer's and payment frames carry their own limits. 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 and every paymentAmount, and the wallet prices each choice from rate and its own gas.
conditionsBinding constraints for the offer. maxGasLimit caps max_gas and maxCost caps max_cost in wei (see Transaction Construction); maxCost also 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 the transaction expires.
tokensOne or more accepted tokens under the offer's shared terms. rate is binding while ttl holds (see Token Payment); payers SHOULD fold their overhead into it, including on an AA transaction their payer_auth cost, which is outside max_gas. paymentAmount is the required amount at gasEstimate, for display and for wallets that use gasEstimate as-is; it is present exactly when gasEstimate is. paymentGas, or paymentLimits for a frame transaction, 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), or for "native" a single value transfer to recipient, 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.
refundWhen 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.
balanceAdvisory 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 provider attribute the source in aggregated responses and SHOULD be omitted otherwise.
providerDisplay info for the party behind the offer or balance, 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 / fiatRatefiatRate is a display-only price of one whole token in fiatCurrency as a WAD (18-decimal fixed-point, like rate), 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.
contextOpaque 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).
calls / framesThe intent, in the shape of the transaction type the wallet will build: calls for an AA transaction, frames for a frame transaction. A request MUST carry exactly one, and a payer that does not support that type returns an empty options. frames are the wallet's own frames in order: any deploy frame, the sender's VERIFY frame approving execution, then one or more SENDER frames for the user's calls. They MUST contain exactly one VERIFY frame, the sender's, so the wallet and payer agree where the pay frame goes. They exclude the expiry, payer, and payment frames, which the wallet adds at construction.
gasLimit / frame limitsThe wallet's gas for the transaction without phase 0: gasLimit for calls, typically its own eth_estimateGas for the user calls, and each request frame's limits for frames. Payers MUST NOT assume these include phase 0: a token offer's gasEstimate.gasLimit is gasLimit plus paymentGas. When they are omitted, the payer SHOULD estimate the user calls' gas from the request's intent and transaction inputs; if the calls revert, it SHOULD return EXECUTION_REVERTED (see Payer Error Codes) rather than price a guess.
Transaction inputsFor AA transactions, 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. For frame transactions, signatures holds placeholders for the sender's entries, at the length real signatures will have. 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; senderActorId and non-secp256k1 senderAuth apply only with Keystore Integration. 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.

For an open offer, whose transactions carry payer = 0x00, the offer's co-sign endpoint takes the place of payer; a response therefore carries at most one open sponsored offer per endpoint. 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 preauthorized offers 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 (5ofa5 of a 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.40),butthisintentwouldcost70,000×3.5×109=2.45×1014wei(0.40), but this intent would cost 70,000 × 3.5 × 10^9 = 2.45 × 10^14 wei (0.49), so the payer declines sponsorship with the cost 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": "COST_EXCEEDS_LIMIT",
      "reason": "Gas above sponsorship cap. We cover up to $0.40 per transaction; this one would cost $0.49 at current prices.",
      "cost": {
        "estimated": "0xDED381F85000",
        "limit": "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 transaction built from an offer, co-signs it, submits it, and returns the transaction hash. The transaction MUST carry the offer's payer with the payer's authorization left empty: on an AA transaction, payer set to the offer's payer and an empty payer_auth; on a frame transaction, the offer's frames with empty bytes in the payer's signature entry. The payer fills it in (see Payer Authorization). A payer that cannot accept the transaction as-is MUST reject it and SHOULD include actionable data, such as minGasLimit or minFrameLimits for GAS_TOO_LOW (see Payer Error Codes).

payer_sendTransaction RPC Specification
type SendTransactionParams = {
  transaction: `0x${string}`; // the sender-signed transaction, with the payer's authorization empty
  context?: Record<string, any>;
};

type SendTransactionResult = {
  transactionHash: `0x${string}`;
};
payer_sendTransaction Example Parameters
{
  "transaction": "0x...",
  "context": {
    "appId": "my-app"
  }
}
payer_sendTransaction Example Return Value
{
  "transactionHash": "0x1234abcd..."
}

payer_signTransaction

Returns the payer's payerAuth for a built transaction without submitting it. The wallet places the bytes as it would an offer's payerAuth: in payer_auth on an AA transaction, or in the payer's signature entry on a frame transaction. The transaction MAY already carry the sender's signature or not: the payer's signature is independent of the sender's, so the user can sign before or after this call. Calling it before the user signs means a payer rejection never follows the user's signature. Otherwise the constraints are those of 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 = {
  transaction: `0x${string}`; // the built transaction, with or without the sender's signature
  context?: Record<string, any>;
};

type SignTransactionResult = {
  payerAuth: `0x${string}`; // the payer's authorization bytes for this transaction
};
payer_signTransaction Example Return Value
{
  "payerAuth": "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 provider) 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
  kinds?: ("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",
      "provider": { "name": "Coinbase One" }
    },
    {
      "kind": "credit",
      "limits": [
        {
          "unit": "asset",
          "available": "0xBEBC20",
          "asset": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
          "symbol": "USDC",
          "decimals": 6
        }
      ],
      "payer": "0xDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDdDd",
      "endpoint": "https://gas.market/v1",
      "provider": { "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",
      "provider": { "name": "Weekly free gas ($0.05 or 3 tx)" }
    }
  ],
  "ttl": 30
}

The third entry illustrates the compound case: a 0.05USDceiling(‘0xC350‘=50000in6−decimalUSD=0.05 USD ceiling (`0xC350` = 50000 in 6-decimal USD = 0.05) and a 3-transaction count, both refreshed weekly (validFor: 604800). The grant is the most-depleted axis — when the sender has used 3 of 3 transactions or at least $0.05, the grant declines (and a payer would surface that as BUDGET_EXHAUSTED or SENDER_LIMIT_REACHED).

Transaction Construction

The request's intent selects the transaction type: calls builds an EIP-8430 AA transaction, and frames builds an EIP-8141 frame transaction. The payer returns offers for that type, and the co-sign methods receive the typed transaction bytes. Everything outside the two subsections below applies to both, through these terms:

TermAA transactionFrame transaction
PayerThe payer fieldThe pay frame's target
Payer authorizationpayer_authThe payer's entry in signatures
Phase 0calls[0]The payment frame
max_gasgas_limitThe transaction's maximum gas, with worst-case bytes (see Frame Transactions)
max_costmax_gas × max_fee_per_gasmax_gas × max_fee_per_gas plus blob gas × max_fee_per_blob_gas
Expiryvalid_beforeThe expiry_verify frame's deadline

Open payer offers, nonce_key, and Keystore Integration exist only for AA transactions.

AA Transactions

The EIP-8430 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
])

call = rlp([to, value, data])

payer is empty for self-pay, a 20-byte address for a named payer, or 0x00 for an open payer; this specification uses the last two (see Payer Authorization).

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, value, data]. Phases execute in order; each phase is atomic (all-or-nothing), but completed phases persist independently. An intent call's value maps directly onto its call. Mempools reject a transaction whose sender cannot cover its total call value at entry, but block validity does not depend on it: if the balance drops before inclusion, the unfunded call reverts its phase and the payer still pays for the gas used, so payers SHOULD simulate before co-signing.

Constructing an AA Transaction

The wallet builds the transaction from the offer it picked and, for a token offer, the TokenChoice it picked:

  1. Set payer to the selected offer's payer: its address, or 0x00 for an open offer.
  2. Set nonce_key and nonce_sequence. Pick a nonce_key (0 for the account nonce, shared with other transaction types; another value for a parallel channel) and read its next nonce_sequence with eth_getTransactionCount and its nonceKey parameter.
  3. 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. Set valid_before = now + expiry, where RECOMMENDED expiry = 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 a valid_before beyond now + maxExpiry with EXPIRY_OUT_OF_BOUNDS.
  4. Set gas parameters (max_fee_per_gas, max_priority_fee_per_gas, gas_limit), typically from the wallet's own estimation run in parallel with payer_getTerms. For a token offer, gas_limit is the user calls' estimate plus the phase-0 gas (paymentGas for the default transfer). A wallet MAY instead use gasEstimate as-is. Either way, before signing the wallet MUST ensure gas_limit covers its own estimate or simulation of the complete transaction, phase 0 included, and MUST respect the offer's conditions.
  5. Construct calls using call phases:
Offer kindPhase 0Phase 1
sponsored(none)[user_calls]
token (ERC-20)[[choice.token, 0, transfer(recipient, amount)]][user_calls]
token ("native")[[recipient, amount, 0x]][user_calls]

recipient is choice.feeRecipient ?? offer.payer, and amount is the required amount for the transaction's own max_cost, 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.

  1. Authorize. Have the sender sign the sender signature hash to produce sender_auth, and obtain payer_auth as in Payer Authorization. The two signatures are independent and may be produced in either order.

Frame Transactions

The EIP-8141 frame transaction wire format is:

FRAME_TX_TYPE || rlp([chain_id, nonce, sender, frames, signatures, fees, blob_versioned_hashes])

frame     = [mode, flags, target, [execution, state], value, data]
signature = [scheme, signer, msg, signature]
fees      = [max_priority_fee_per_gas, max_fee_per_gas, max_fee_per_blob_gas]

The wallet assembles the transaction around its request frames, in the order the EIP-8141 public mempool recognizes when the payer is not the sender:

FrameMode and flagsTargetSource
expiry_verifyVERIFY, noneEXPIRY_VERIFIERWallet, REQUIRED
deploy (optional)DEFAULTFactoryWallet
Sender verificationVERIFY, APPROVE_EXECUTIONsenderWallet
payVERIFY, APPROVE_PAYMENTOffer's payeroffer.frames.pay
Payment (token offers)SENDER, nonechoice.token, or recipient for "native"Wallet
User framesSENDERPer callWallet
postOp (optional)DEFAULToffer.frames.postOp.targetoffer.frames.postOp
  • The expiry_verify frame is REQUIRED and first, with its deadline set as for an AA transaction's valid_before.
  • The pay frame directly follows the sender's VERIFY frame, with zero value and offer.frames.pay's data and limits. A postOp frame, when present, follows the user frames.
  • For a token offer, the payment frame directly follows pay and plays the role of phase 0: transfer(recipient, amount) on choice.token, or for "native" a frame to recipient with value amount. Its limits are choice.paymentLimits or the wallet's own, and it MUST NOT carry ATOMIC_BATCH_FLAG.
  • signatures[0] is the sender's entry. When offer.frames.signature is present, signatures[1] is the payer's, with its scheme and signer, an empty msg, and either the offer's payerAuth or empty bytes for the payer to fill.
  • Before signing, the wallet MUST ensure every frame's limits cover its own simulation of the complete transaction, and MUST respect the offer's conditions.

Frames are placed relative to one another rather than at fixed indices; other orders can be added later as a field in frames (see Field Notes).

Worst-case bytes. For max_gas, every byte of the payment frame's data and of each signature entry counts as nonzero, at its scheme's length (65 bytes for SECP256K1, 128 for P256) or, for ARBITRARY, the length of its bytes or placeholder, so the wallet and payer derive the same max_gas before either signs.

Token Payment

This specification pins 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 max_cost and the choice's rate:

required = ceil(max_cost × choice.rate / 10^18)

For an AA transaction max_cost is gas_limit × max_fee_per_gas. Implementations MUST compute it with exact integer arithmetic, multiplying before dividing and rounding up once (equivalent to Solidity mulDivUp(max_cost, 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, or for a frame transaction a single payment frame, that pushes the payment from the sender to the recipient. For an AA transaction:

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(max_cost, choice.rate, 1e18)   // the required amount
phase_0   = choice.token == "native"
  ? [[recipient, amount, 0x]]
  : [[choice.token, 0, abi.encodeCall(IERC20.transfer, (recipient, amount))]]

For an ERC-20 token this is IERC20.transfer, a push from the sender, so no token approval is needed, unlike ERC-4337 paymasters that pull payment after an approve(). The payer verifies it by inspecting the call, without simulation. It is the RECOMMENDED construction and the only one a "transfer"-mode payer accepts.

Native payment. A TokenChoice whose token is "native" (symbol and decimals those of the chain's native asset) is paid with a plain value transfer. It suits payers that sell something besides gas, such as private or priority inclusion; its rate is typically 10^18 plus the payer's margin. Keystore integration forbids value from some senders (see Keystore Integration).

Richer constructions (paymentMode: "any"). A payer MAY accept any phase-0 calls that produce the credit: for example a transferFrom from a funding account, a session-key module call, a privacy-pool withdrawal that splits proceeds between the recipient and the sender, or a non-standard token's transfer method. Several calls MAY compose the payment; only the net credit to recipient is constrained. These constructions can reintroduce approvals or funding dependencies and fail at inclusion in ways a direct transfer cannot, a tradeoff the wallet and payer accept.

Payer verification is policy. In either mode the payer confirms the credit (by inspection for "transfer", by simulation for "any"), applies its own risk checks, such as sender reputation, velocity limits, or funding-source checks, and then co-signs or rejects. This specification requires only that the credit land.

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, a WAD; in the running example, 0.21 USDC at a fiatRate of 0xDE0B6B3A7640000 (1.00 USD) is $0.21. Without a refund this is the final charge; with one it is an upper bound.

Payer Authorization

The payer's authorization reaches the transaction in one of three ways. To the wallet the bytes are always opaque: it places them unmodified.

SourceSender signsSubmitted by
payer_sendTransactionBefore the payerPayer
payer_signTransaction, returning payerAuthBefore or after the payerWallet
The offer's payerAuth (Preauthorized Payers)AfterWallet

When co-signing an AA transaction, the payer validates the transaction against its terms and signs the payer signature hash (AA_PAYER_TYPE, see EIP-8430). A named payer's payer_auth is authenticator || data, which without Keystore Integration is K1_AUTHENTICATOR || signature by the payer account's key. An open payer's is a 65-byte secp256k1 signature. The payer hash covers the resolved sender and the same fields as the sender's, so neither signature depends on the other, and a change to any signed field invalidates both. Nothing the payer does affects gas_limit or any field the sender signs: payer_auth is excluded from both hashes and its gas is metered outside gas_limit, charged to the payer.

On a frame transaction, the payer signs the canonical signature hash into its entry at signatures[1]. Entries with an empty msg are left out of that hash, so either party may sign first, while the sender's signature still covers the payer's frames and the entry's scheme and signer. Unlike payer_auth, the payer's verification gas is in its pay frame's limits, and so in max_gas.

A payer service MAY delegate signing to a separate signing service that custodies the key, for example to separate pricing and policy from key management and submission. For a named payer, sender_auth binds the transaction to the payer account regardless of which operator runs the endpoint.

Open Payer Offers

Open offers exist only for AA transactions. An offer whose payer is 0x00 is open: the wallet sets the transaction's payer to 0x00, and the payer co-signs with any secp256k1 key it controls. sender_auth then commits to no particular payer, which gives the wallet two things:

  • One signature across sponsors. A transaction with no phase 0 is identical for every open sponsor. If an open sponsored offer's co-sign is rejected, the wallet MAY present the same signed transaction to another open sponsored offer whose conditions it satisfies, including one returned in the rejection's options, without the sender re-signing. The sender MAY also pay for it directly by signing the payer hash with its own key, which resolves to self-pay.
  • Key rotation for payers. A payer can sign from a pool of keys, spreading per-payer mempool limits and rotating keys without changing its quotes.

Rules for open offers:

  • Open mode is secp256k1 only, so a payer that authenticates with any other authenticator uses named offers.
  • Every TokenChoice in an open token offer MUST set feeRecipient, since there is no payer address to default to. The recipient, not the signing key, is what binds the payment.
  • The co-sign fingerprint uses the offer's co-sign endpoint in place of the payer address (see Co-Sign Disambiguation Invariant).
  • A wallet SHOULD present a signed open transaction to one payer at a time. If several co-sign, only one version can be included, because they share the nonce (or replay_id), and a replacement needs a fee bump.

Preauthorized Payers

A preauthorized offer needs no co-sign: it carries payerAuth, authorization bytes the payer provides up front and valid for every transaction that satisfies the offer, or, for a frame transaction, frames with no signature because its pay frame needs none. A payer does this when its authorization does not depend on seeing each transaction, for example a verifier or paymaster contract that checks the phase-0 credit against its own price feed. The wallet builds the transaction from the offer as usual, places payerAuth if present, signs, and submits it through the chain's ordinary transaction path (eth_sendRawTransaction); it calls no co-sign method unless the offer lists one. Preauthorization works with either kind.

For the wallet, payerAuth is opaque, and how the payer produced it does not matter:

  • The wallet MUST place the bytes unmodified and MUST simulate the complete transaction with them in place before submitting. No payer service checks the transaction, so a failed simulation means the transaction does not satisfy the offer.
  • For a token offer, rate is the payer's quote of what its authorization accepts while ttl holds. The wallet SHOULD add headroom when the payer prices from a live feed.

For the payer:

  • The authorization is public. Anyone can attach it to any transaction naming this payer, so a payer MUST NOT publish a preauthorized offer unless it is willing to pay for every transaction the authorization validates. In practice this means an authenticator or paymaster that enforces payment and policy onchain; a signature over one transaction validates only that transaction and belongs in a signing response, not an offer.
  • Chain support. On an AA transaction, reusable bytes need an authenticator the chain accepts, which in practice means Keystore Integration with the Transaction Context precompile populated during authentication. On a frame transaction, the pay frame's paymaster can inspect the transaction's frames and read the state it prices against itself, but the EIP-8141 public mempool limits how many pending transactions a non-canonical paymaster may have. A payer SHOULD publish preauthorized offers only where the chain supports its authorization.
  • Quotes carry a buffer. Authorization that prices from shared state, such as a price feed, can reject a pooled transaction after that state moves. A payer SHOULD quote rate with 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 (one entry of options, in a response with the running example's gasEstimate):

{
  "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.

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:

  • -32000 PAYER_REJECTED — every payer-domain rejection from payer_sendTransaction / payer_signTransaction uses this envelope. data is a PayerRejectedData (below): the specific condition in data.code (string) plus any actionable detail: a requote (for PAYMENT_INSUFFICIENT), a minGasLimit or minFrameLimits (for GAS_TOO_LOW), a revert (for EXECUTION_REVERTED), a shortfall (for SENDER_BALANCE_INSUFFICIENT), a cost diagnostic, a balance, or fresh options to switch to.
  • Standard JSON-RPC codes (-32600 Invalid Request, -32601 Method not found, -32602 Invalid params, -32603 Internal error, -32700 Parse 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, and -32000 with data.code set to TEMPORARILY_UNAVAILABLE if the payer cannot evaluate the intent, or to EXECUTION_REVERTED (with revert) if the user calls revert when the payer estimates them.

type PayerErrorCode =
  // Decline scope: `sponsored_declined.code` and send/sign errors
  | "POLICY_REJECTED"
  | "SENDER_INELIGIBLE"
  | "BUDGET_EXHAUSTED"
  | "SENDER_LIMIT_REACHED"
  | "COST_EXCEEDS_LIMIT"
  | "TEMPORARILY_UNAVAILABLE"
  // Send/sign: faults in a built transaction (`EXECUTION_REVERTED` also on `payer_getTerms`)
  | "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"
  | "COST_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
  cost?: {                     // for COST_EXCEEDS_LIMIT
    estimated: `0x${string}`;
    limit: `0x${string}`;
  };
  minGasLimit?: `0x${string}`; // for GAS_TOO_LOW on an AA transaction: the smallest `gas_limit` the payer's simulation needs
  minFrameLimits?: Limits[];   // for GAS_TOO_LOW on a frame transaction: the smallest limits per frame, in the transaction's frame order
  requote?: { // for PAYMENT_INSUFFICIENT: the phase-0 credit the payer accepts now
    token: `0x${string}` | "native";
    paymentAmount: `0x${string}`;   // the minimum sufficient credit now, at this transaction's `max_cost`
    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.codeChannelPermanent?Description
POLICY_REJECTEDdeclined + send/signYesThe calls, contracts, or sender are outside this sponsor's policy for this intent. Pay with a token or use a different payer.
SENDER_INELIGIBLEdeclined + send/signYesSender is rejected entirely (blocklisted, or missing a required attestation).
BUDGET_EXHAUSTEDdeclined + send/signNoSponsor budget or prepaid credit is depleted; balance.validFor indicates when it refills.
SENDER_LIMIT_REACHEDdeclined + send/signNoPer-sender cap reached (tx count or USD); balance.validFor indicates when it refills.
COST_EXCEEDS_LIMITdeclined + send/signNoThe transaction's cost at current gas prices exceeds this payer's per-transaction ceiling. The payer SHOULD include the cost diagnostic. Retry when gas prices are lower.
TEMPORARILY_UNAVAILABLEdeclined + send/sign + getTerms errorNoPayer is degraded (e.g. rate feed down). Retry shortly.
INVALID_TRANSACTIONsend/signn/aMalformed or invalid transaction, or one not built from the offer as Transaction Construction requires.
UNSUPPORTED_TOKENsend/signYesToken not accepted by this payer.
PAYMENT_INSUFFICIENTsend/signNoPhase-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_LOWsend/signNogas_limit, or a frame's limits, is too low for phase 0 or the user calls. The payer SHOULD include minGasLimit, or minFrameLimits for a frame transaction; the wallet raises the gas (within conditions.maxGasLimit), recomputes any token amount, and re-signs.
EXECUTION_REVERTEDsend/sign + getTerms errorNoThe payer's simulation or estimate shows a call would revert, so it will not quote or 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_BOUNDSsend/signNoThe transaction's expiry is beyond the offer's conditions.maxExpiry, or a frame transaction has no expiry_verify frame.
SENDER_BALANCE_INSUFFICIENTsend/signNoThe 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_INSUFFICIENTsend/signNoPayer 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 or frames. A rejection carries no gasEstimate; each paymentAmount is instead REQUIRED and priced at the rejected transaction's max_cost, with a frame offer's own frames in place of the rejected offer's. 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 max_cost, gas-price movement does not cause this error; a rate that moved after ttl does. Wallets MUST treat it as an expected loop:

  1. Catch PAYER_REJECTED with data.code === "PAYMENT_INSUFFICIENT".
  2. With a requote, re-sign phase 0 crediting requote.paymentAmount to requote.feeRecipient (default: the offer's payer) and go to step 4.
  3. Without one, call payer_getTerms again, re-select the same (token, feeRecipient) choice if possible, recompute the amount from its fresh rate, and re-sign.
  4. 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.

Payer Sources

A wallet finds payers in two places: an app's paymasterService URL and payers the wallet configures itself. Both serve the same methods, and the wallet combines them as described in Aggregation.

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-8430 and EIP-8141 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-8430 or EIP-8141 transaction, the wallet follows the flow with the app's URL as one of its payer sources. 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

A wallet MAY also select payers itself, with no app involvement: a payer service it runs or contracts with, configured per chain. When an app's payer is also available, the wallet decides which to use, for example preferring app sponsorship and falling back to its own token payer.

Aggregation

Several payer sources combine in two ways, both using only the methods above: for example a wallet-configured payer that accepts stablecoins, an app's URL that may sponsor, and a subscription the user holds an allowance with.

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: its estimate for the user calls plus the phase-0 gas, and for a frame transaction the payer's frames, with the payment the required amount for that transaction's max_cost. 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). For a named payer, and every frame transaction payer, the sender's signature binds the chosen payer, so the wallet commits to exactly one payer when it signs; an open offer leaves the payer unbound.

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.

Keystore Integration

Everything above works on any chain with the EIP-8430 transaction type, using secp256k1 senders and payers only. This section applies to AA transactions where the EIP-8130 Keystore is also integrated.

  • Sender actors. payer_getTerms accepts senderActorId and a non-secp256k1 senderAuth placeholder, so the payer can evaluate the actor's scope and policy and price its authentication. Wallets SHOULD pass senderActorId when the actor is policy-gated.
  • Payer actors. A named payer co-signs with an actor holding SPONSOR_PAYER on the payer account. When an open transaction's payer resolves to the sender, the paying actor needs SELF_PAYER instead.
  • Authenticators. The canonical authenticator set is accepted on every chain. Payers that co-sign with any other authenticator depend on the chain's acceptance policy, and SHOULD use canonical authenticators where they can.
  • Value rules. A policy-gated sender actor, or a sender whose code is canonical account bytecode, MUST send zero value in every call. Such senders cannot use a "native" TokenChoice or intents that move value; payers SHOULD NOT offer native payment to them, and wallets MUST NOT select it for them.
  • Reusable payerAuth. Bytes valid for many transactions need a non-secp256k1 authenticator, and therefore this integration (see Preauthorized Payers).
  • High-rate payers. Nodes MAY give higher pending-transaction limits to payer accounts with canonical account bytecode, which suits payer services that co-sign at volume.

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, except in an offer's frames, where a wallet skips the offer instead (see Field Notes). 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-8430, 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_sendTransaction minimizes hops: the payer co-signs and submits in one step. This is the common case for app-sponsored transactions.
  • payer_signTransaction gives the wallet submission control, useful for high-value transactions, MEV-sensitive paths, or private mempools. Because it also accepts a transaction the sender has not signed yet, the wallet can have the user sign last, so a payer rejection never follows the user's signature.

They are kept as two methods rather than one method with a submit/relay flag because their return types genuinely diverge — a transactionHash versus payerAuth — 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 methods supports payer_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.

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. With the EIP-8130 Keystore, payers need not be 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.

Conditional Inclusion for Submitting Payers

A payer that lists only payer_sendTransaction keeps the co-signed bytes and chooses how they reach a builder, so no third party can broadcast them to a competing builder or the public mempool. Where a chain offers conditional inclusion, for example a submission endpoint that holds a transaction until given state predicates hold and drops it if they fail or expire, such a payer MAY condition inclusion on phase 0 succeeding, such as the sender's token balance still covering the payment. A payment that would fail is then never included, so the payer pays no gas for it and the sender's nonce is not consumed. These are submission choices, not wire-protocol additions; to a wallet, such a 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. The transaction's expiry 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 max_cost, 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 covers phase 0 but not the calls pays the payer while the calls revert out of gas. A payer that understates paymentGas, paymentLimits, 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 the transaction's gas covers its own estimate or simulation of the complete transaction, phase 0 included, and SHOULD rank offers on its own gas figures. The check is one simulation after the user picks, which a wallet typically runs anyway to preview balance changes. 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 or minFrameLimits.

Frame transactions: The payer's frames never run as the sender: VERIFY frames are static calls, and only the wallet's SENDER frames can move the sender's assets. Their limits count toward max_gas, so wallets SHOULD compare them across offers as they compare paymentGas. The EIP-8141 public mempool caps pending transactions for paymasters that are not canonical paymaster instances, so payers co-signing at volume SHOULD use canonical instances.

Open payers: An open transaction's sender_auth commits to no payer, so any secp256k1 key may pay for it, and a third party that sees it pending may attach its own payer_auth as a fee-bumped replacement. The body and sender are unchanged, so the only effect is who pays. For sponsorship this is harmless; for token payment the phase-0 recipient is fixed by the sender's signature. Wallets that need a specific payer, for example for exclusive inclusion, use named offers.

Preauthorized payers: A payerAuth published on an offer 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. Submitting payers MAY additionally keep phase-0 failures from being included (see Conditional Inclusion for Submitting Payers). 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. 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 and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth