Why
A protocol-native preview of what this project's application-layer AA demos (session keys, atomic batching) do today with smart contracts and delegation — EIP-8141 proposes moving those same properties into Ethereum's base transaction format itself.
How it works
Not a working demo by necessity: EIP-8141 defines a new transaction type where a single transaction carries a sequence of frames (a VERIFY frame for signature/fee authorization, then one or more EXECUTE frames) instead of one implicit call — but no client or RPC can send this transaction type yet, since it requires execution-layer support the network doesn't have. As of writing it's only "considered for inclusion" in a future fork, so this stays a diagram/explainer page rather than a live demo.
The ACDE readout, and the two kinds of "no" (2026-08-28)
The call was set to conclude between this proposal and EIP-8130, and as of writing no public readout confirms an outcome. What the developer timeline did in that gap is the useful part: the argument moved off the content and onto the weight. The recurring sentence was "the direction is right, but this is too heavy for the current stage" — and that is a schedule objection, not a technical one.
The distinction is worth holding onto because the two look identical from outside and demand opposite responses:
| The objection | What it actually says | What you do next |
|---|---|---|
| "This is wrong" | the design does not achieve the goal | change the design |
| "This is not now" | the design is right and too large to land here | split the scope |
Hearing the second and redesigning is wasted work; hearing the first and merely deferring is worse. The question to ask in the room is which one you are being given — and the tell is usually whether the objection survives when you make the proposal smaller.
Review clarification
The basic concept, from zero
Ethereum has two kinds of accounts, and only the dumb one can act. An EOA can start transactions, but its rules are frozen into the protocol: one ECDSA key, one sequential nonce, fees paid by the sender in ETH, one call per transaction — lose the key, lose the account. A contract account is fully programmable but cannot initiate anything. Account abstraction is the project of erasing that split: let the account that acts also be programmable — in who may sign, how fees are paid, and what one transaction may contain.
Today's AA is an emulation built one layer up: ERC-4337 rebuilt a transaction system out of contracts (UserOperations, bundlers, an EntryPoint), and EIP-7702 lets an EOA borrow contract code. It works, at the price of extra actors, extra gas, and extra trust surfaces. EIP-8141 proposes ending the emulation: a new base transaction type made of frames — one VERIFY frame ("here is the proof this is authorized, and how fees get paid") followed by one or more EXECUTE frames (the calls, atomically: all or nothing). The two jobs every transaction always had — prove it's allowed, then do things — become explicit, separate, programmable parts of the format itself.
The same action, three generations of code
Today, the node's checks are hard-coded and unchangeable:
assert(ecrecover(tx.sig) == tx.from); // exactly one ECDSA key
assert(tx.nonce == account.nonce); // exactly one nonce
assert(balance[tx.from] >= gas * price); // sender pays, ETH only
call(tx.to, tx.data); // exactly one call
ERC-4337 replays those checks inside a contract — EntryPoint.handleOps loops over UserOperations calling account.validateUserOp and then account.execute, with a bundler carrying the batch and a paymaster optionally paying. Every hop is the emulation tax.
Under EIP-8141 the node itself runs the loop the EntryPoint used to fake:
tx = { type: FRAME_TX, frames: [
{ kind: VERIFY, target: myAccount,
input: { scheme: "p256-passkey", proof, feeToken: USDC } },
{ kind: EXECUTE, target: USDC, data: approve(dex, 100e6) },
{ kind: EXECUTE, target: dex, data: swap(USDC, ETH, 100e6) },
]}
and the account's own verify code is where the features live — a session key, natively:
function verify(Proof p, Frame[] fs) external view returns (bool) {
if (p.signer == owner) return checkSig(p);
Session s = sessions[p.signer]; // a temporary key
require(block.timestamp < s.expiry); // ...that expires
require(allCallsTo(fs, s.allowedContract)); // ...only this app
require(totalValue(fs) <= s.perTxLimit); // ...small amounts
return checkSig(p);
}
Who it makes happy
| Who | Today's pain | With frame transactions |
|---|---|---|
| Users | Seed phrase or bust; approve-then-swap leaves a dangling approval; need ETH first | Passkey as the native key; approve+swap atomic; fees in tokens or sponsored |
| Wallet builders | Run or rent bundler + paymaster infrastructure | Those services largely disappear; the chain validates directly |
| dApp/game/agent builders | Session keys need a smart-wallet stack per provider | Bounded delegation is a base-layer feature |
| The protocol | A parallel 4337 mempool; EntryPoint as a shared choke point | One canonical mempool, no single trusted contract |
Concrete situations it improves: the dangling-approve accident (two EXECUTE frames make approval-without-swap impossible); onboarding without ETH (a sponsor pays inside VERIFY); lost-key recovery as account logic instead of a custodian's feature; the post-quantum migration (signature schemes become account code to upgrade, not protocol constants to fork); and agent commerce (an AI agent holding a session key with expiry, an allowed contract, and a per-transaction limit is delegation with a chosen blast radius).
The honest cost — and most of why ACDE said "too heavy": the VERIFY frame is arbitrary code deciding mempool validity before anyone has paid, so it must be strictly gas-bounded and cheaply re-checkable, or invalid transactions become a free DoS vector.
"Then ERC-4337 could be sunsetted?" (jay, 2026-09-03)
Right direction, wrong tense. 4337 was always the stopgap — enshrinement was the stated endgame, and 8141's VERIFY/EXECUTE split is essentially validateUserOp/execute promoted into the transaction format. So 4337 doesn't get killed; it gets absorbed — the concepts survive as the contracts empty out. But the sunset is a decade-scale decay, for four reasons: nothing on Ethereum is ever removed (the EntryPoint is immortal, and millions of deployed and counterfactual smart-account addresses don't migrate themselves); the native-AA competition is unsettled (8141 vs 8130 vs Tempo — a workaround with users beats a proposal without a fork date); L2s break the symmetry (4337 works identically on every EVM chain today, while native AA lands chain by chain — cross-chain wallets may be its last stronghold); and the bundler/paymaster vendors will pivot to the new rails rather than defend the old ones, accelerating the very sunset that obsoletes them. As strategy — don't build a moat out of 4337 plumbing — the sentence is already true. As a forecast — traffic to zero — add ten years and an asterisk.
Progress (2026-09-08)
Vitalik's 9/6 Frames update reported movement: on the 8/27 call the proposal went CFI → SFI (considered → scheduled for inclusion), slotted for Hegotá (targeted 2027 Q2) alongside FOCIL. SFI is an assignment, not a completion — the tell to watch after the 9/10 devnet-priority list is whether a devnet actually attaches to 8141. Two framing notes. The headline 'pay gas in stablecoins' is a consequence, not the design: the design separates the three jobs that today share one signer — authorization, gas payment, execution — and stablecoin fees are what fall out once gas payment is its own strand inside VERIFY. And the competition is asymmetric on timing: rival EIP-8130 targets a Base deployment in September, so the L2 implementation becomes real before the L1 standard does — a workaround with users, again, outrunning a proposal with a fork date.
The same runtime-vs-spec point the invariant card makes applies to the objection here: liquid-issuance-not-authorization is why an enshrined VERIFY frame must be strictly gas-bounded and cheaply re-checkable — arbitrary validity code that runs before anyone pays is a premise the network cannot afford to trust unconditionally.
Related code
# ERC-8141 — a transaction carries a sequence of frames instead of one implicit call:
# a VERIFY frame (signature/fee authorization) followed by one or more EXECUTE frames.
# Simulates the frame structure and runs it in sequence, aborting if VERIFY fails.
from dataclasses import dataclass, field
from typing import Callable
@dataclass
class Frame:
kind: str # "VERIFY" or "EXECUTE"
description: str
run: Callable[[dict], bool]
@dataclass
class FrameTransaction:
frames: list[Frame] = field(default_factory=list)
def execute(self, context: dict) -> bool:
for frame in self.frames:
print(f" [{frame.kind}] {frame.description} ...", end=" ")
ok = frame.run(context)
print("OK" if ok else "FAILED")
if frame.kind == "VERIFY" and not ok:
print(" -> VERIFY frame failed: transaction aborted before any EXECUTE frame runs.")
return False
if frame.kind == "EXECUTE" and not ok:
print(" -> EXECUTE frame failed: transaction reverts.")
return False
return True
def verify_signature_and_fee(ctx: dict) -> bool:
return ctx.get("signature_valid", False) and ctx.get("balance", 0) >= ctx.get("max_fee", 0)
def execute_transfer(ctx: dict) -> bool:
ctx["balance"] -= ctx["transfer_amount"]
return ctx["balance"] >= 0
def execute_approve(ctx: dict) -> bool:
ctx["allowance"] = ctx.get("allowance", 0) + ctx["approve_amount"]
return True
def build_transaction() -> FrameTransaction:
return FrameTransaction(frames=[
Frame("VERIFY", "signature + fee authorization", verify_signature_and_fee),
Frame("EXECUTE", "transfer 50 tokens", execute_transfer),
Frame("EXECUTE", "approve spender for 20 tokens", execute_approve),
])
if __name__ == "__main__":
print("EIP-8141 Frame Transaction — VERIFY then EXECUTE, EXECUTE, ...\n")
print("Case 1: valid signature, sufficient balance")
ctx = {"signature_valid": True, "balance": 100, "max_fee": 5, "transfer_amount": 50, "approve_amount": 20}
ok = build_transaction().execute(ctx)
print(f"Transaction succeeded: {ok}, final state: {ctx}\n")
print("Case 2: invalid signature — VERIFY frame blocks all EXECUTE frames")
ctx = {"signature_valid": False, "balance": 100, "max_fee": 5, "transfer_amount": 50, "approve_amount": 20}
ok = build_transaction().execute(ctx)
print(f"Transaction succeeded: {ok}")