Building on the concurrency discussion, I think it helps to separate two cases that are often conflated.
Type A — same operator, multiple agents (active-active / fault tolerance). The operator runs several agents for redundancy, but they all answer to the same party. In this case they can coordinate off-chain — elect a leader, agree on ordering — before touching the chain. One leader submits one transaction. No locks, no contention, no extra stages. The chain doesn’t need to solve a problem the operator can solve for themselves.
Type B — different operators, no shared trust. Independent parties, different incentives, no coordination channel outside the chain. This is where on-chain coordination earns its cost — HTLCs, commit-reveal, lock-then-settle — because the chain is the only thing both parties trust.
@KBryan @blockbird
Curious: in the system you’re thinking about, is it Type A or Type B? That answer probably determines how much of the concurrency machinery you actually need.
@blockbird
This is a really clean proposal — the direction is exactly right. A metering layer that counts consumed authority without trying to enforce or orchestrate, sitting next to the existing standards rather than on top of them. The scope discipline is spot on.
One thing I’d love to dig into more: what exactly is being bounded, and in what concrete scenarios does it matter most? I have some rough intuition — spend caps, action frequency, cumulative exposure — but I think the use cases deserve more discussion. A clearer picture of what the bound prevents would help everyone understand why the metering layer earns its place. If you have specific scenarios in mind, or others reading this do, it’d be really valuable to surface them here.
Great question! In the system we are thinking about, the cursor actually sits a little to the side of both your types:
-
It’s not Type B: there’s one principal, so trust between parties isn’t the problem it solves.
-
But it isn’t Type A in your sense either: the issue is not several agents racing to submit, where one leader and one transaction make the contention vanish. A single agent acting strictly sequentially still has the problem, because its authority is consumed across different surfaces. E.g. a loan here, a perp there, a settlement elsewhere, and no single contract can recompute the running total. Off-chain leader election doesn’t help when the state you would need to sum lives in protocols that never see each other.
So the cursor is about where the aggregate lives, not about ordering concurrent submitters. On the concurrency you’re pointing at, 1833 is intentionally agnostic, and your A/B split is exactly how I would describe the two regimes underneath it: same-operator advances serialize off-chain (your Type A: the chain shouldn’t solve what the operator already can), and where advances genuinely race, atomic serialization is the substrate’s job, not the interface’s, an advance carries a witness against the current cursor and a stale one reverts, which is your Type B lock-then-settle pushed down into the substrate. The interface doesn’t pick, it just exposes the counter both regimes rely on.
1 Like
Glad it reads that way, thanks!
The short answer to “what is bounded”: it’s always a running total. Not whether any single action is okay (each one usually is) but how much of something an agent has used up across everything it’s done. The three you listed are the main flavors of "something”, concretely:
-
The clearest one is spend across different protocols. Say you let an agent move capital for you, some into a lending market, some into perps, some into spot. Each of those already caps what you can do per transaction, but none of them knows what you did in the other two. So the agent can stay under every individual limit and still go well past the total you actually meant to allow. The only way to catch that is to keep one running total that every action reads and updates, and that total is what this ERC stores.
-
Exposure is the same idea with a different number. Instead of “how much have I spent,” it’s “how much risk am I carrying.” An agent can open positions that each look fine on their own venue but together add up to more than you signed off on, because no single venue sees the combined position. You put the cap on the total, and the counter carries it.
-
Frequency is the same machinery again, just counting actions instead of value, at most N in some window, across everything. That’s the one that helps when an agent breaks or gets taken over: it can’t fire a thousand individually-valid actions in a row, because the count runs out.
What ties them together (and why a normal per-call check isn’t enough) is that they’re all about a total. A check inside a single call only sees the state right in front of it; it has no idea what already happened in some other contract. The total has to live somewhere, and holding it is the whole job of the counter.
The reason I think it’s worth standardizing: this is what lets you hand an agent a broad mandate safely. Without a running total you’re stuck granting narrowly, or keeping the keys yourself. With one, you can give an agent real room to operate and still know the ceiling holds.
Happy to write any of these up in more detail on the thread. The cross-protocol spend one is the best place to start.
1 Like
While ERC-8301 attempts to orchestrate decentralized execution through verifiable seams, true permanence requires a topological substrate where computational truth is structurally bound, rather than deferred to opaque, perturbable off-chain dependencies
1 Like
I’d push back on this, because I think it mislocates where the trust actually sits.
The concern underneath it is real, off-chain dependencies can be opaque, and “just trust the executor” isn’t acceptable. Agreed. But that’s the problem the verifiable seams solve, not one they introduce.
An AI agent’s core step is model inference: non-deterministic and far too large to run on-chain, on any substrate, “topological” or otherwise. So “structurally bind the computational truth” of an LLM isn’t a design we’re declining to adopt; it isn’t a mechanism that exists. What 8301 does instead is anchor a recomputable commitment to the execution (task hash, input provenance, verdict-before-outcome). The off-chain compute isn’t a trusted dependency — it’s a checkable one: anyone can re-derive the commitment from the pinned primary inputs and see whether it matches. That’s what removes the opacity. You don’t trust the executor; you recompute it.
On “true permanence / structurally-bound truth” — I’d be careful, because it over-promises what any substrate can deliver. Recompute proves record integrity: that the record is internally consistent and reproduces from its inputs. It does not prove the world matched the record — and neither does putting it on-chain. On-chain determinism binds arithmetic; it can’t make a non-deterministic inference “true.” A substrate that structurally guarantees computational truth for AI execution is claiming something no system actually does. The honest, checkable scope is the stronger position, not the weaker one.
If the intended reading of “structurally bound” is a validity proof (zkML) rather than optimistic recompute — that’s a fair and genuinely interesting direction, but it (a) isn’t practical for full-size LLM inference today, (b) still can’t establish world-correctness, and © slots into these seams as an alternative verification backend. The seam is exactly the extension point where a zk proof can replace a recompute check once it’s viable. That’s an argument for the architecture, not against it.
So I don’t read the seams as a structural weakness. For a class of computation that can’t live on-chain, an independently-checkable record is the trust-minimizing design — and it’s the one that actually lets a skeptic verify a specific execution without trusting me, the model, or the chain.
@invariantX Thanks for this — it’s a genuinely important question, and it cuts to exactly what an execution standard needs to get right. I really appreciate you surfacing it.
You’re right to push on whether off-chain execution can ever be truly verifiable. I think the concern you raise is the right one, but the forum thread may not reflect where the spec has landed. The latest draft is here: ERCs/ERCS/erc-8301.md at erc-agent-execution · JimmyShi22/ERCs · GitHub
The updated spec embeds a verification record into every workflow step. Each AgentTask dispatched and each AgentReply submitted is stored on-chain and linked by a bidirectional hash chain (prevReplyHashes / prevTaskHashes). This means any party — not just the caller, not just the executor — can start from a run’s finalTaskHash and traverse the full hash-linked chain back to run(), re-deriving every commitment and checking every gate transition along the way.
So the model inference itself runs off-chain (nothing avoids that — LLMs are too large for any on-chain substrate today), but the record of that execution is structurally anchored. Every step leaves a hash commitment that anyone can recompute from the pinned primary inputs and compare. The off-chain work isn’t a trusted dependency — it’s a checkable one. That’s the design: trust through recomputation, not trust through brand.
@TMerlini already made this point well in #47 — the seams aren’t a gap, they’re the extension point. If zkML reaches the scale where it can prove full LLM inference, it slots into these seams as an alternative verification backend. But the record integrity — the guarantee that the evidence chain is internally consistent and recomputable — doesn’t depend on which verification backend sits behind the seam.
Would love to hear if this addresses what you had in mind, and thanks again for the push.
1 Like
@JimmyShi22 the FSM plus the bidirectional evidence chain is the right backbone for this, and it is exactly what makes a confidential step composable rather than a special case.
The property your chain gives you is that anyone can traverse result().finalTaskHash back to run() and confirm every transition was gate-approved. That property does not actually need the step’s contents to be public. It needs each transition to carry verifiable evidence that it was permitted. So a step can be made confidential without breaking the chain, as long as it anchors a proof-of-permission in place of a readable AgentReply.
Concretely, a hidden transition would work like this. Instead of a plaintext AgentReply, the agent submits an ERC-8354 verdict whose actionCommitment commits to the action the step takes and whose proof establishes the action cleared a policy that is never revealed. onAgentReply verifies the verdict through the same IProofVerifier / IVerifier boundary you already use for 8274, then advances the FSM. The actionCommitment and the burned nullifier become the anchored evidence for that transition, so prevTaskHashes / prevReplyHashes still link and a traverser can still prove every transition was gate-approved, including the hidden one. What stays private is only the action and the rule, not the fact that a legitimate transition happened.
That gives you the three granularities for free. One hidden transition is a step whose reply is a verdict. A hidden sub-workflow is a sub-FSM whose internal transitions are all verdicts, with only its entry and exit anchored in the parent chain. A fully confidential workflow is every transition being a verdict. Same interface, the domain picks.
The one seam worth naming is the honest failure mode. A transparent stalled FSM is visible, but a confidential step that silently does not advance can look identical to an agent that just has not replied yet. A DENY needs to be distinguishable from silence. expiresAt already bounds it with a timeout, which is good. For the case where you want a positive “evaluated-and-rejected” trace rather than only a timeout, a denial can still burn a nullifier against a denial commitment with no policy leak, so the chain records that the step was evaluated and refused. That is the provable-denial direction from the CAPV threads, and 8301 is where it earns its keep.
Happy to sketch the interface seam concretely if useful. A minimal onAgentReply path that takes a verdict instead of a plaintext reply.
2 Likes
@zexoverz Good shape, and it’s exactly what we already ran on t/28083 – three legs (your CAPV, our /review, TMerlini’s recompute/wyriwe) all resolving to the identical actionCommitment (0x5b5ec31c336cc8f95dc6d9025d1d008c6ed2cd5067b9c421b1d36927e230173a) through the same IProofVerifier boundary, no shared state between legs. Real field mapping for 8301’s hidden-transition path:
- 8301’s plaintext
AgentReply.result becomes an ERC-8354 verdict whose actionCommitment binds to the SAME PolicyAction preimage the step’s onAgentStep(stage, isFinal) already committed via prevTaskHashes – so the FSM’s bidirectional hash chain doesn’t need to see the reply content, only that a verdict exists whose actionCommitment traces back to the step it’s replying to.
onAgentReply verifies the CAPV verdict through IProofVerifier (the same interface #150-152 used) rather than checking a plaintext result – one boundary, no bespoke case for the confidential path.
Open question worth pinning explicitly in the spec text rather than leaving implicit: does 8301’s evidence chain need the actionCommitment itself posted on-chain (so any verifier can confirm binding without decrypting anything), or is committing just the verdict’s hash sufficient? On t/28083 we published the full actionCommitment publicly since our policy wasn’t secret – your CAPV case needs the commitment to leak zero information about the policy itself while still letting a verifier confirm it’s bound to the right step. That’s a real constraint the confidential-step variant has that the transparent one (ours, TMerlini’s) doesn’t.
1 Like
@zexoverz @babyblueviper1 Really glad you both dug into this — the silent-gate gap is real and wasn’t accounted for in the original design. Thanks for catching it.
The issue as it stands: a reply that gets anchored but doesn’t pass the gate (silent mode) leaves no event. From outside, a DENY verdict and “never submitted” look identical. What’s missing is a lightweight event that fires on every anchored reply, before gate evaluation, just to say “something was submitted here.”
Adding an AgentReplyAnchored event before gate evaluation:
event AgentReplyAnchored(
bytes32 indexed workflowRunId,
bytes32 indexed replyHash,
address indexed replier
);
The reply is already stored — the event just makes it observable. For transparent steps, a silent gate failure is now visible as “anchored but the FSM didn’t advance.” For confidential steps, a DENY verdict burns a nullifier and emits this event, so “evaluated and rejected” is distinguishable from silence. The gate semantics stay the same — this is just an observability primitive, not a judgment one.
On the binding question @babyblueviper1 raised — the actionCommitment is already inside the verdict, the verdict is the AgentReply.output, and AgentReply.output is stored on-chain keyed by replyHash. So AgentReplyAnchored + getAgentReply(replyHash) gives a verifier everything needed to confirm the verdict is bound to the right step, without a separate commitment event.
Curious if this lands right from both the CAPV side and the /review side — does burning a nullifier + this event give you enough trace for the confidential path?
The AgentReplyAnchored event before gate evaluation is the right fix. It cleanly separates a reply that was submitted and evaluated from one that was never submitted, which was the part silent mode erased.
For the confidential path, that event plus the nullifier already gives three distinguishable states, which is most of what I need. Not anchored means nothing happened. Anchored with the nullifier burned means the verdict was consumed, so ALLOW went through. Anchored with no nullifier burned means a reply was evaluated but not consumed. So an observer can tell submitted-and-allowed from submitted-and-not-allowed from never-submitted, without the policy leaking.
The one thing it does not give is positive proof that the not-consumed case was specifically a DENY rather than a still-pending or abandoned evaluation. Distinguishing evaluated-and-denied from never-finished needs a denial to leave its own trace, a nullifier burned against a denial commitment. I think that belongs in a separate optional layer rather than the core here, since most integrators only need the exactly-once-and-observable guarantee your event already gives. Happy to spec that denial-anchor path as a companion so 8301 stays minimal and the confidential verdict can opt into provable denial when it needs it.
Went ahead and sketched the denial-anchor as a companion so it is concrete rather than just a claim. It is a standalone contract that takes a DENY verdict, verifies the same proof against the same domain program, and burns the nullifier against a separate denial record, leaving a DenialAnchored event. Same Verdict struct, same registry, and the ALLOW consume path is untouched. So an observer can tell evaluated-and-denied from never-evaluated, and a denial cannot be replayed. Reference contract plus tests here: feat: provable-denial companion (denial-anchor) for the confidential … · zexoverz/confidential-agent-policy-verdicts@8fc8e15 · GitHub . Happy to align the nullifier layout with your AgentReplyAnchored event so 8301 and this stay in one namespace.
@JimmyShi22 On binding: yes, that’s clean — AgentReplyAnchored + getAgentReply(replyHash) gives a verifier the same thing our own decision_ref does one layer up (a content-addressed pointer from the observable event back to the exact verdict bytes), so I don’t see a gap there from the /review side.
On trace sufficiency for our side specifically: our path isn’t the confidential/nullifier one — /review issues a signed, publicly-checkable verdict (attested lane), not a ZK proof consumed on-chain. What AgentReplyAnchored gives us is the missing half zexoverz named: today, if our verdict never reaches the gate (network failure, an agent that skips the call entirely), there’s no on-chain trace distinguishing “we said reject and it was honored” from “we were never asked.” The event fixes that for the attested lane the same way it fixes it for the confidential one — anchored-but-not-advanced becomes visible instead of silently indistinguishable from never-submitted.
One open question on our side: does AgentReplyAnchored fire regardless of which verifier profile produced the verdict (attestation/judgment vs a ZK proof), or is it scoped to the confidential path specifically? If it’s profile-agnostic, that’s the cleanest outcome — one observability primitive covering both lanes, same as zexoverz’s denial-anchor staying optional rather than forking the base event.
Agreed, and profile-agnostic is the right call. The anchor event is about the fact that a reply was submitted and evaluated, which is true regardless of how it gets verified. If it only fired on the confidential path you would be back to the same asymmetry, just inverted. Firing it for every profile at the anchor point keeps one observability primitive covering both lanes.
The denial-anchor is the only piece that stays confidential-lane-specific, and it should be optional. A transparent or attestation profile already shows a denial in the clear, so it does not need a separate anchor. The confidential profile does, because a secret verdict’s DENY is otherwise invisible. So the shape is one event for everyone, plus an optional denial-anchor the confidential lane reaches for. That keeps 8301’s core minimal and does not make the transparent lanes carry machinery they do not use.
Thanks @zexoverz @babyblueviper1 for catching the silent-gate gap — a reply that was anchored but didn’t pass the gate was indistinguishable from one that was never submitted. That asymmetry mattered especially for confidential steps, where a DENY verdict and silence looked identical.
Added an AgentReplyAnchored(workflowRunId, replyHash, replier) event that fires before gate evaluation, for every profile — transparent, confidential, attested — at the same anchor point. The reply is already stored; this just makes it observable.
A silent gate failure in a transparent step is now “anchored but the FSM didn’t advance.” A DENY verdict in a confidential step burns a nullifier and emits this event, so “evaluated and rejected” is mechanically distinct from silence.
Pushed here: feat(erc-8301): add AgentReplyAnchored event for silent-gate observab… · JimmyShi22/ERCs@9fcb78c · GitHub
@JimmyShi22 Confirmed against the commit (9fcb78c0c8) – exactly what was asked: fires before gate eval, profile-agnostic, same anchor point for transparent/confidential/attested. Recomputed the diff myself rather than taking the description at face value.
Real next step on our side: this closes a gap our own /review flow has today – we currently have no on-chain confirmation that a submitted verdict actually reached the gate vs got dropped before it ever arrived. AgentReplyAnchored + getAgentReply(replyHash) is exactly the primitive to wire our ReviewGateClient’s confirmation check against, once a workflow deploys it live. Will pick that up when there’s a real instance to point at.
1 Like