--- eip: 8168 title: Payer Service Capability description: How apps, wallets, and payer services arrange gas payment for EIP-8430 and EIP-8141 transactions author: Chris Hunter (@chunter-cb) discussions-to: https://ethereum-magicians.org/t/erc-8168-payer-services-for-erc-8130/28762 status: Draft type: Standards Track category: ERC created: 2026-02-19 requires: 5792, 7677, 8130, 8141, 8430 --- ## Abstract [ERC-7677](./eip-7677.md) defines the `paymasterService` capability for [EIP-5792](./eip-5792.md), 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](./eip-8430.md) AA transactions or [EIP-8141](./eip-8141.md) 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](./eip-8430.md) 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](./eip-4337.md) 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](./eip-8141.md) 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](./eip-7677.md) 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](./eip-8430.md) transaction type, with plain secp256k1 accounts, and extends to the [EIP-8130](./eip-8130.md) Keystore where it is integrated. The same methods serve [EIP-8141](./eip-8141.md) 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: | 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 a transaction, signed by the sender or not yet, and return `payerAuth` | | `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"]`, unless the offer is [preauthorized](#preauthorized-payers), 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](#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](#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 ```typescript // [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[]; // 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; // 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`: ```typescript 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: ```typescript 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](./eip-20.md) 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](#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](#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` / `endpoint` | `payer` is the paying account: on an AA transaction the value of its `payer` field, which is `0x00` for an [open offer](#open-payer-offers), 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. | | `methods` | The co-sign methods callable against the offer's `(payer, endpoint)`; see [Payer Service Interface](#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` | Authorization 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](#preauthorized-payers). | | `frames` | The payer's part of a frame transaction, which the wallet places unmodified (see [Frame Transactions](#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. | | `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: 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. | | `conditions` | Binding constraints for the offer. `maxGasLimit` caps `max_gas` and `maxCost` caps `max_cost` in wei (see [Transaction Construction](#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](#transaction-construction)); payers SHOULD keep it to a few blocks, since they remain liable for gas until the transaction expires. | | `tokens` | One or more accepted tokens under the offer's shared terms. `rate` is binding while `ttl` holds (see [Token Payment](#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 `feeRecipient`s (see [Co-Sign Disambiguation Invariant](#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. | | `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 `provider` attribute the source in aggregated responses and SHOULD be omitted otherwise. | | `provider` | Display 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 `` to prevent script execution. | | `fiatCurrency` / `fiatRate` | `fiatRate` 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. | | `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`). | | `calls` / `frames` | The 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 `limits` | The 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](#payer-error-codes)) rather than price a guess. | | Transaction inputs | For 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](#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](#open-payer-offers), 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 `feeRecipient`s, 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 ```json { "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. ```json { "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 ($5 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. ```json { "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. ```json { "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), 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`. ```json { "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](#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-error-codes)). ##### `payer_sendTransaction` RPC Specification ```typescript type SendTransactionParams = { transaction: `0x${string}`; // the sender-signed transaction, with the payer's authorization empty context?: Record; }; type SendTransactionResult = { transactionHash: `0x${string}`; }; ``` ##### `payer_sendTransaction` Example Parameters ```json { "transaction": "0x...", "context": { "appId": "my-app" } } ``` ##### `payer_sendTransaction` Example Return Value ```json { "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 ```typescript type SignTransactionParams = { transaction: `0x${string}`; // the built transaction, with or without the sender's signature context?: Record; }; type SignTransactionResult = { payerAuth: `0x${string}`; // the payer's authorization bytes for this transaction }; ``` ##### `payer_signTransaction` Example Return Value ```json { "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 ```typescript 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; // 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. ```json { "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.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](./eip-8430.md) **AA transaction**, and `frames` builds an [EIP-8141](./eip-8141.md) **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: | Term | AA transaction | Frame transaction | | --- | --- | --- | | Payer | The `payer` field | The `pay` frame's target | | Payer authorization | `payer_auth` | The payer's entry in `signatures` | | Phase 0 | `calls[0]` | The payment frame | | `max_gas` | `gas_limit` | The transaction's maximum gas, with worst-case bytes (see [Frame Transactions](#frame-transactions)) | | `max_cost` | `max_gas × max_fee_per_gas` | `max_gas × max_fee_per_gas` plus blob gas × `max_fee_per_blob_gas` | | Expiry | `valid_before` | The `expiry_verify` frame's deadline | [Open payer offers](#open-payer-offers), `nonce_key`, and [Keystore Integration](#keystore-integration) exist only for AA transactions. #### AA Transactions The [EIP-8430](./eip-8430.md) AA transaction wire format is: ```text 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](#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](#open-payer-offers). 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 `kind` | Phase 0 | Phase 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](#token-payment) 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. 6. **Authorize.** Have the sender sign the sender signature hash to produce `sender_auth`, and obtain `payer_auth` as in [Payer Authorization](#payer-authorization). The two signatures are independent and may be produced in either order. #### Frame Transactions The [EIP-8141](./eip-8141.md) frame transaction wire format is: ```text 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](./eip-8141.md) public mempool recognizes when the payer is not the sender: | Frame | Mode and flags | Target | Source | | --- | --- | --- | --- | | `expiry_verify` | `VERIFY`, none | `EXPIRY_VERIFIER` | Wallet, REQUIRED | | `deploy` (optional) | `DEFAULT` | Factory | Wallet | | Sender verification | `VERIFY`, `APPROVE_EXECUTION` | `sender` | Wallet | | `pay` | `VERIFY`, `APPROVE_PAYMENT` | Offer's `payer` | `offer.frames.pay` | | Payment (`token` offers) | `SENDER`, none | `choice.token`, or `recipient` for `"native"` | Wallet | | User frames | `SENDER` | Per call | Wallet | | `postOp` (optional) | `DEFAULT` | `offer.frames.postOp.target` | `offer.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](#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`: ```text 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: ```text 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](./eip-4337.md) 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](#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. | Source | Sender signs | Submitted by | | --- | --- | --- | | `payer_sendTransaction` | Before the payer | Payer | | `payer_signTransaction`, returning `payerAuth` | Before or after the payer | Wallet | | The offer's `payerAuth` ([Preauthorized Payers](#preauthorized-payers)) | After | Wallet | 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](./eip-8430.md)). A named payer's `payer_auth` is `authenticator || data`, which without [Keystore Integration](#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](#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](#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](./eip-8141.md) 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`): ```json { "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. ```typescript 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.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. | | `COST_EXCEEDS_LIMIT` | declined + send/sign | No | The 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_UNAVAILABLE` | declined + send/sign + getTerms error | No | Payer is degraded (e.g. rate feed down). Retry shortly. | | `INVALID_TRANSACTION` | send/sign | n/a | Malformed or invalid transaction, or one not built from the offer as [Transaction Construction](#transaction-construction) requires. | | `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`](#re-quote-on-payment_insufficient). | | `GAS_TOO_LOW` | send/sign | No | `gas_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_REVERTED` | send/sign + getTerms error | No | The 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_BOUNDS` | send/sign | No | The transaction's expiry is beyond the offer's `conditions.maxExpiry`, or a frame transaction has no `expiry_verify` frame. | | `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](#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](#payer-service-interface), 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: ```json { "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](#aggregation). #### `paymasterService` Capability This proposal reuses the `paymasterService` capability from [ERC-7677](./eip-7677.md) unchanged: apps pass a `url` and optional `context` on an [EIP-5792](./eip-5792.md) `wallet_sendCalls` (or [ERC-7836](./eip-7836.md) `wallet_prepareCalls`) request, exactly as specified there. For [EIP-8430](./eip-8430.md) and [EIP-8141](./eip-8141.md) transactions the wallet uses the `payer_*` methods defined here; for [ERC-4337](./eip-4337.md) user operations it uses the `pm_*` methods from [ERC-7677](./eip-7677.md). 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](./eip-5792.md) `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](./eip-8430.md) or [EIP-8141](./eip-8141.md) transaction, the wallet follows the [flow](#payer-service-interface) with the app's URL as one of its payer sources. The same methods apply to [ERC-7836](./eip-7836.md) `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](#token-payment) 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](#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](#open-payer-offers) 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](#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](./eip-8430.md) transaction type, using secp256k1 senders and payers only. This section applies to AA transactions where the [EIP-8130](./eip-8130.md) 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](#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](#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](./eip-7677.md)'s two-phase `pm_getPaymasterStubData` / `pm_getPaymasterData` flow exists because [ERC-4337](./eip-4337.md) paymasters inject data into the UserOperation before gas estimation, and the stub data must produce consistent code paths and byte lengths. In [EIP-8430](./eip-8430.md), 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](./eip-8130.md) 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](./eip-8130.md) 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](./eip-7677.md). 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](./eip-8141.md) 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](#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](#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 Copyright and related rights waived via [CC0](../LICENSE.md).