ERC-8226: Regulated Agent Mandate

UPDATE (June 29, 2026): Spec refinements and reference implementation now live in PR #1844. See the update post below for the full change list and our position on the custody open question.


Authors:** Ludovico Rossi, Dario Lo Buglio, Thamer Dridi, Nabil El Alami Khalifi
GitHub PR: Add ERC: Regulated Agent Mandate by thamerdridi · Pull Request #1679 · ethereum/ERCs · GitHub
Status: Draft


RAMS defines a compliance delegation layer for AI agents operating on tokenized regulated assets. It is agnostic to the agent identity system and the token compliance framework: it works with any agent registry that maps wallet addresses to agent identifiers (such as ERC-8004) and any regulated token standard that implements a pre-transfer compliance check (such as EIP-7943 or ERC-3643).

No existing standard addresses what happens when an AI agent executes a transaction on a regulated financial instrument — a tokenized security or real-world asset subject to KYC, AML, or investor eligibility requirements under frameworks such as MiCA, VARA, or MiFID II.

RAMS closes this gap by specifying:

  1. Mandate delegation. How a verified principal delegates scoped, time-bounded, and financially capped authority to an on-chain agent — analogous to a power of attorney in traditional finance.

  2. Dual compliance check via pre-transfer hook. How RAMS-aware regulated tokens verify mandate validity through their existing pre-transfer compliance hook, executing two checks in sequence: first the token’s own investor eligibility on the principal (the token’s compliance module is never bypassed), then the RAMS mandate validity on the agent. The token issuer retains full sovereignty over who may hold or transact their instrument.

  3. Unified compliance provider. A single IComplianceProvider interface for principal eligibility verification (identity + compliance in one call), with structured reason codes for audit trails.

  4. Jurisdiction-scoped enforcement. Two-tier enforcer roles (PLATFORM vs REGULATORY) with freeze authority scoped by jurisdiction. Global freeze is restricted to REGULATORY tier only.

The standard defines two interfaces: IComplianceProvider (external compliance contract) and IAgentMandate (RAMS registry). Each agentId has exactly one active principal at a time, mirroring the account segregation requirements of MiCA (Article 70), MiFID II (Article 16), and VARA. Agents use standard ERC-20 transfer and transferFrom — no agent-prefixed function variants are required on the token side. No modifications to any existing standard are needed.


Open topics

The following topics are intentionally left open for community input:

Token ownership and custody model. When an agent acquires tokens on behalf of a principal, who is the registered owner? Two models are under consideration:

  • Agent-custodied: tokens are held by the agent wallet. Beneficial ownership is tracked via the RAMS registry and ExecutionRecorded events. Suits high-frequency trading strategies where the agent needs direct control.
  • Principal-custodied: tokens are transferred or minted directly to the principal’s address. The agent orchestrates the trade but settlement is principal-to-principal. Produces a clean investor registry by default. Suits long-term investment strategies.

Both models are compatible with RAMS mandate verification. We are seeking input on whether the standard should prescribe a default or remain agnostic.

Receive-side compliance. The current specification covers agent-initiated outbound transfers (selling, moving). For inbound transfers (buying), the token’s canTransact must verify the receiver. If the receiver is the agent wallet rather than the principal address, the token must determine whether to apply investor eligibility checks to the agent or to the principal. This is closely related to the custody model.

regulatoryCompliance trust signal in ERC-8004. We suggest a regulatoryCompliance value in the supportedTrust field of the ERC-8004 registration file, as a signal that an agent operates under a certified mandate for regulated transactions. This would require no changes to the ERC-8004 core spec.


We welcome feedback on the dual compliance check pattern, the IComplianceProvider interface, the one-active-principal constraint, and the open topics above.

9 Likes

One question I still have is about the compliance/verifier layer.

The proposal explains how a compliance provider can validate principals and support RAMS enforcement, but what are the safeguards around the compliance providers themselves?

If these providers become the trust anchor for regulated agents, how do we prevent capture, corruption, or excessive centralization over time? In other words, who verifies the verifiers?

1 Like

@TrendAdmin Great question, and it goes to the heart of the trust architecture. RAMS addresses this at three levels:

  1. Principal sovereignty over provider selection.

The compliance provider is not a system-wide singleton. The principal selects the complianceProvider address at grantMandate time. This means multiple compliance providers can coexist and compete. A principal working under MiCA may choose a regulated EU KYC provider; another operating under VARA may choose a different one. If a provider is compromised or underperforms, the principal revokes the mandate and re-issues it with a different provider. The market disciplines the providers, not a central authority.

  1. Two-tier enforcement independent of provider state.

RAMS defines a two-tier enforcer model (PLATFORM and REGULATORY). The registry operator assigns these roles to specific addresses as part of deployment. An address with REGULATORY tier can freeze an agent globally regardless of what the compliance provider reports. This means the enforcement mechanism exists independently of the compliance provider: even if a provider is captured and keeps returning COMPLIANT, an enforcer can halt the agent directly through the RAMS registry. In practice, platforms operating under regulated frameworks (MiCA, VARA) would be expected to assign REGULATORY tier access to the competent authority as part of their licensing obligations.

  1. The token’s own compliance is never bypassed.

*This is the most important safeguard. RAMS adds a second compliance layer but does not replace the first. The token’s own investor eligibility check (via its pre-transfer hook) runs on the principal before RAMS mandate validity is even evaluated. A compromised RAMS compliance provider cannot grant access to a token whose issuer has independently determined that the principal is ineligible. The token issuer retains full sovereignty.

What RAMS intentionally does not prescribe is the internal governance of a compliance provider: how it manages its KYC data, what audit standards it follows, or how its upgrade mechanism works. The spec recommends (SHOULD-level) that principals select providers with audited, time-locked upgrade mechanisms and documented uptime SLAs. But the standard does not enforce this because compliance provider governance is a regulatory matter, not a protocol matter. Different jurisdictions will impose different requirements on these entities, and RAMS should not constrain that.

The short answer to “who verifies the verifiers”: the principal (by choosing and switching providers), the registry operator (by assigning enforcement roles), and the token issuer (by never ceding their own compliance checks). No single actor has unchecked authority.

That said, this is exactly the kind of design tension we want pressure-tested. If you see a scenario where these three layers still leave a gap, we would rather hear it now than after deployment. What attack vector concerns you most?*

1 Like

Nice work on RAMS. The compliance delegation model maps cleanly to what we’re building with ERC-8240.

Concrete composability: a RAMS IComplianceProvider can consume IAttestation.getAttestation(subject) as one eligibility input — the compliance check becomes quality-aware without changing either interface. Add IRiskSignal.getRegime() as a second input: same principal might pass under STABLE regime but get restricted under CRISIS.

Two calls, two interfaces, one composable trust decision. No coupling.

Happy to spec out the integration pattern if useful.

Hey Nicopat, really like this framing. a

The good news is IComplianceProvider is already designed to support exactly this. Since we only constrain the output of the interface and not how a provider reaches its decision, an ERC-8240 backed provider that aggregates attestations and risk signals works today without any changes to ERC-8226. Implementers are free to compose whatever inputs make sense for their jurisdiction. That composability being possible without spec-level coupling is actually something we’re pretty intentional about. Keeps the standard focused and lets the ecosystem experiment freely on top of it.

1 Like

Hi hi

@Ludovico.r, Nabil

clean architecture on the three-tier safeguard.

A small composition note from our side: ERC-8240 (Trust Infrastructure for Agents and Assets) operates at the consumer/quality layer above RAMS / token compliance via your IComplianceProvider, agent quality via ERC-8240’s IAttestation. Two checks, two interfaces, no overlap with the RAMS base layer.

On the open regulatoryCompliance signal in ERC-8004 supportedTrust: this maps cleanly to what IAttestation produces. Happy to flesh out a short composition note if useful

same approach as the one I’m drafting for ERC-7943, will share the two together.

Patrick

Interesting proposal. I particularly like the decision to keep RAMS agnostic to both the agent identity framework and the regulated token standard. By positioning itself as a delegation and mandate layer rather than another compliance framework, it appears capable of integrating with existing ecosystems such as ERC-3643, EIP-7943, and ERC-8004 without requiring issuers to replace their current infrastructure.

The dual compliance check pattern also makes sense to me. Preserving the token issuer’s existing eligibility checks before evaluating mandate validity helps ensure that RAMS acts as an additional authorization layer rather than a mechanism that could bypass issuer-defined compliance requirements.

One area where I would appreciate further discussion is the one-active-principal constraint. While I understand the motivation from an account segregation and regulatory perspective, I wonder how this model would accommodate institutional use cases where a regulated portfolio manager, robo-advisor, or AI investment service acts on behalf of multiple principals under separate mandates. Is the expectation that such operators deploy a dedicated agent identity per principal, or is there a future path for supporting multiple principals while preserving segregation guarantees?

Regarding the open topics, I tend to favor the principal-custodied model as the default for regulated RWAs. It aligns naturally with investor registries, beneficial ownership reporting, and existing compliance workflows. However, I can also see the value of keeping the standard agnostic, particularly for higher-frequency strategies where agent-controlled custody may be operationally preferable. If the standard remains agnostic, it may be useful to define a canonical way for integrators to determine the principal, agent, legal owner, and beneficial owner associated with a transaction so that custodians, issuers, and compliance providers can interpret ownership consistently.

Overall, I think the proposal fills an important gap. Existing standards address identity, compliance, and tokenization independently, but there is currently no standardized way to express and verify delegated authority for AI agents interacting with regulated assets. RAMS appears well positioned to address that missing layer.

1 Like

@yehia67 thanks, two points worth tightening.
One active principal: it’s one mandate per (agent, principal) pair, not per agent. The same agent can hold N mandates from N principals concurrently, so robo-advisors and multi-client operators work natively, no dedicated agent identity per client. We’ll make that explicit in the next update.

On custody: RAMS stays custody-agnostic by design. The mandate defines authorization between agent and principal; it lets the agent act against the principal’s wallet, never take custody. Principal-custodied is the default we recommend for regulated RWAs (aligns with investor registries and beneficial-ownership reporting), but the standard doesn’t hardcode it, since custody varies by structure and resolves through the compliance model.

Canonical role resolution: RAMS resolves four on-chain roles today, agent, principal, compliance provider, and enforcer, with identityRef linking to off-chain identity via the provider. Legal vs beneficial owner is the real open part: the principal can be either depending on structure (trust company vs HNWI). We lean to extend the IComplianceProvider with optional getLegalOwner(principal, identityRef) and getBeneficialOwner(principal, identityRef), keeping IAgentMandate minimal and delegating the ownership model to the provider rather than adding opinionated fields to the mandate struct.

Does that cover your use cases? If not, what’s the scenario where resolving through the provider falls short, keen to understand where you’d want it at the mandate level.

Nice, the delegation/mandate layer is exactly the piece that’s been missing. Everyone did identity and tokenization, nobody really did “what is the agent actually allowed to do”.

Read through the spec and one thing I couldn’t find: replay protection on the grant signature. grantMandate just says verify the signature against principal, but there’s no nonce or domain binding on what gets signed. The “already has an active mandate” guard only holds while the mandate is active, so once a principal calls revokeMandate, isActive goes false and the agent has no active mandate again. Doesn’t that let someone replay the principal’s original grant signature and re-instate the mandate they just revoked? Same worry across chains or registry deployments if the signed payload isn’t pinned to chainId + the registry address.

Intended to be in scope here, or left to implementations?

Great catch @VladKuzR, thanks for reading through the spec, and yes it’s in scope.
The current update adds full replay protection on all signed operations: a per-principal nonce that’s included in the signed payload and consumed on use, plus a deadline. So a grant signature is spent the first time it’s used; after a revoke it can’t be replayed because the nonce has already advanced. Cross-chain / cross-registry replay is prevented by the EIP-712 domain, which binds chainId and verifyingContract (the registry address). EIP-1271 is supported for contract-wallet principals.
So nonce + deadline + domain binding, normative for every signed op. The version you read predated this; the update is landing now!
Does this address the concern you had, or do you see another edge case we should make explicit in the spec?

Update: spec refinements and reference implementation now in PR #1844

We’ve opened PR #1844. Two sets of changes, one from the EIP review process and one from this thread.

Spec refinements

  • complianceProvider is now mandatory at grantMandate. The optional path is removed. This closes a gap that was implicit in @TrendAdmin’s question on safeguards: with the provider optional, a mandate could be granted without principal eligibility verification ever running. Making it mandatory ensures the dual compliance check is always active.

  • Cap denomination clarified, and enforcement is now explicitly venue-agnostic. The same mandate can be enforced at the token hook, in an EIP-7702 delegated account, or via a dedicated executor, with consistent semantics across all three.

Reference implementation (under assets/erc-8226/)

  • AgentMandate registry

  • ComplianceProvider reference

  • Optional AgentExecutor

  • End-to-end integration with ERC-7943 (uRWA-20) showing the dual compliance check pattern in working code

Position on the custody model open question
After working through the reference implementation, the current PR keeps the spec custody-agnostic. Prescribing a default would constrain adoption across use cases with materially different operational needs (high-frequency strategies vs long-term investment vehicles, agent-direct vs principal-direct settlement). Both models are supported by the same interfaces. The reference implementation demonstrates the agent-custodied path with ExecutionRecorded events for traceability; principal-custodied works without any spec changes. We’ll revisit this position once we have production deployments to learn from.

Open for feedback

  • Does the venue-agnostic enforcement model hold up for EIP-7702 delegated accounts in your view, or are there account-abstraction edge cases that need explicit handling in the spec?

  • On composability: @Nicopat, your ERC-8240 integration note remains fully applicable. IComplianceProvider is unchanged. Happy to coordinate on the composition note you mentioned.

1 Like

Hi guys,

At BlueCore Studio we have a proposal: reason codes for the mandate layer.

canExecute gives you a bool. When it comes back false you can’t tell a revoked mandate from an expired one, or an over-cap amount from a wrong asset. Your check order separates those cases, the result just doesn’t leave the function. So anyone who wants to show a user or an agent a real message reimplements that order and guesses at it, and two integrators guessing separately end up saying different things. ReasonCode on the compliance side answers whether the principal is eligible, a different question from why the mandate check failed.

We started with a bytes32 short string, so a registry could mint its own codes when it needed to. After talking it through with the Brickken guys we landed on an enum. Gas didn’t decide it: nothing here gets stored, and either way the value ends up as a full word in the ABI, so the choice came down to standardization. canExecute has a known, closed set of rejection reasons, and once this gets adopted widely, two registries returning different codes for the same condition costs more than the flexibility buys. OTHER at the end covers the extensibility we wanted from bytes32.

We mapped the codes directly onto the canExecute pseudocode in the spec, one value per branch, in the same order:

enum ExecutionReason {
    OK,
    NO_MANDATE,
    TOKEN_NOT_ALLOWED,
    OUTSIDE_VALIDITY_WINDOW,
    REVOKED,
    ACTION_NOT_ENABLED,
    AGENT_FROZEN,
    OVER_TX_CAP,
    OVER_CUMULATIVE_CAP,
    OTHER
}

The spec should say you report the first failing check. Leave that open and two registries answer differently for the same call when several conditions fail at once, and then you can’t compare the codes across implementations.

OTHER is the escape hatch. A registry with a condition outside the set returns OTHER instead of picking whichever neighbour looks closest, and emits its own detail in an event if it needs it.

New values get appended, never inserted, never renumbered, so old integrators keep reading the same thing.

That leaves the question of where the reason comes out, since canExecute returns a single bool today. Which option is right depends on how far integration has gone already.

While the surface is still small, we’d change the signature:

function canExecute(...) external view returns (bool allowed, ExecutionReason reason);

One function, one implementation of the checks, and the only code that has to change is what’s built against the spec so far. The more adoption, the heavier this change would be.

If it’s too late for that, we can leave canExecute as it is and add a second view next to it:

function canExecuteWithReason(...) external view returns (bool allowed, ExecutionReason reason);

canExecute then becomes a thin wrapper over it, so the checks still live in one place and nothing breaks.

If we’re agreed on the enum, I’ll write the spec text for the next PR.

2 Likes

Following up on Vlad’s post above, same team. The reason-code proposal didn’t come from reading the spec, it came from integrating it. Here is that work.

What we built

We wrote a token-side 8226 integration from the published spec on July 8, no contact with the Brickken team beforehand. It is live on Ethereum Sepolia and transacting against the reference registry at 0xD68E1bb972cA4EF7F5764FBf6d685a6DfC26778e. Three contracts, all source-verified:

  • GatedUSDRams, a RAMS-aware ERC-20: 0xd501D68214503Fa03B5179F556029CD15D7f7cAa

  • VARComplianceProviderAdapter, an IComplianceProvider: 0x7302C8ee3E3f53cD85E0BAF1bDe8479DD19575EB

  • DelegationMirror, the delegation registry the adapter reads from: 0x415e267C3C2B1835667b4aDda731599a4B847A3b

The end-to-end flow

A mandate granted on the reference AgentMandate with our adapter as complianceProvider, then an agent-initiated transferFrom through our token, then a blocked one.

grantMandate   0xe5dfe2fbf900d41e0122743bf7a36ab7c4b1bfdd4aa82af6ee3a9ebd9b78ec54
transferFrom   0x796a690853f9c79b71c6dd52892c9e42da447eac9a08fca7329528236869ec6c   (cleared)
transferFrom   0xdfd1877a8e5fed2c910f9ec0bcab93c8409d015ff38ea2cb80feb40931f51d74   (blocked, status 0)

cumulativeUsed read at block 11411043, after the grant and before the transfer: 0. Read at latest: 90000000. The block boundary isolates the transfer as the cause, and the registry’s own ExecutionRecorded event at 11411044 corroborates it. The blocked call reverts RamsBlocked carrying RAMS_OVER_TX_CAP, cumulativeUsed stays put, the sink balance does not move.

We also rebuilt the reference implementation out-of-tree with the deployed toolchain (solc 0.8.30, optimizer 200, OZ 5.6.1) and diffed runtime bytecode against eth_getCode. All three contracts identical apart from the CBOR metadata tail. So the Sepolia deployment everyone can test against is the reference, byte for byte. We found no vulnerability in the contract code.

One property worth stating in the spec: hasRole(RECORDER_ROLE, ourToken) is false and recordExecution succeeded anyway, through the msg.sender == m.asset branch. An outside integrator needs no privileges on the registry to conform. That is a good property and it is not obvious from the spec text.

What the bare bool cost us

This is the concrete case for the enum. canExecute collapses eight failure branches into false, and those branches demand opposite responses from an agent operator: re-issue, wait, fix a bug, retry smaller, or stop until the next mandate.

Our workaround, GatedUSDRams.ramsDiagnose, re-derives the failing check in the registry’s evaluation order and returns one of ten bytes32 codes. It is what produced RAMS_OVER_TX_CAP in the blocked receipt above. The cost: a second getMandate read plus up to three more external calls on the revert path, it only works because the mandate struct is fully public, and it silently rots if the check order ever changes. An integrator should not have to mirror the registry’s control flow to tell a user why a transfer failed.

Given Thamer’s note that 8226 is still draft, we’d change the signature rather than add a second view:

function canExecute(...) external view returns (bool allowed, ExecutionReason reason);

One function, one implementation of the checks. The spec should also require reporting the first failing check in the documented order. Leave that open and two registries answer differently for the same call when several conditions fail at once.

Three more observations from the integration

checkPrincipal’s expiresAt is discarded at grant time. AgentMandate.sol:69 drops both reason and expiresAt, and the Mandate struct has no field for the latter. Storing it and checking it in canExecute would bound the revocation-to-freeze window to the KYC expiry the provider already publishes, no enforcer required, no extra runtime external call. Our integration takes the other route and re-checks checkPrincipal live on the execution path, which closes the window to zero blocks at our asset but helps nobody else. Both seem worth having.

Record persistence on revoke. The reference revokeMandate flags rather than deletes, which is what makes the permissive integration pattern safe. The spec does not require it. The reference canTransfer falls through to plain allowance rules when getMandate(...).principal == address(0), so an implementer who deletes the struct on revoke (the obvious gas optimization) turns revocation into a permission upgrade for any agent still holding an allowance. We hit exactly this trap in our own registry design. One normative line closes it: MUST NOT delete the mandate record on revoke.

No per-agent aggregate across principals. An agent holding mandates from N principals has N independent budgets and no ceiling on the sum. This may be the right design, since per-principal caps mean each principal controls only their own risk. Raising it as a question, not a defect. If deliberate, one sentence in Rationale saves integrators the derivation.

Our own limits, stated plainly

Symmetry matters in a review like this, so the same bar applies to us. Our compliance provider is personhood-attested by a registered attestor key, not trustlessly World-ID-verified. AgentBook lives on World Chain and no canonical state root of chain 480 exists on Sepolia, so the stronger claim was never available. The attestor is a single EOA, no multisig, so every eligibility verdict downstream of it is bounded by one key. grantPrincipal and revokePrincipal revert unconditionally in our adapter, a deliberate deviation since the spec declares both without optionality. None of it is audited.

During this work we also caught a bug in our own code: revoke() left the mandate nonce unchanged, so a dead mandate could be re-bound and the revocation latch reset. Fixed, with regression tests. Same class of trap as the record-persistence item above: state that looks current because one field was not advanced.

Offers

Happy to PR any of these, in whatever order is useful:

  • expiresAt in the mandate struct plus the check in canExecute, with tests

  • the reason-code change, based on what ramsDiagnose already does

  • the agent-custodied integration pattern as a second documented example, with tests (the published pattern only demonstrates principal-custodied, and agent-custodied, where the agent holds the funds and msg.sender == from, is the primary case for autonomous agent payments)

  • text for the record-persistence clause

The full review with a paste-and-run verification appendix went to Ludovico and Thamer on August 3. One caveat if you rerun it: the historical read at block 11411043 needs an archive node. publicnode returns a historical-state error, drpc serves it.

1 Like

Independently re-verified before commenting, not just read: pulled the three receipts against a public Sepolia RPC.

  • grantMandate: 0xe5dfe2fb..., status 1, to the registry 0xD68E1bb9..., block 11411043.
  • Cleared transferFrom: 0x796a6908..., status 1, to GatedUSDRams, block 11411044.
  • Blocked transferFrom: 0xdfd1877a..., status 0 (reverted), same contract.

All three match your account exactly. Also pulled the current merged spec text (erc-8226.md post-#1844) and the canExecute pseudocode really does collapse those eight branches (no-mandate / wrong-asset / outside-window / revoked / action-not-enabled / frozen / over-tx-cap / over-cumulative-cap) into one bool, same as you and Vlad describe — the enum proposal maps onto it one branch at a time, in order, which is the right shape for a closed, standardizable set.

One structural note from the other side of this same shape: we run a synchronous pre-action gate + after-the-fact signed record on unrelated actions (an independent judgment verdict before, a recomputable proof after), and the lesson that cost us the most was the same one Vlad’s OTHER fallback is solving here — a caller needs to be able to tell “the check ran and said no” from “the check couldn’t be run” without those two collapsing into the same bit. allowed=false, reason=OTHER reads correctly as the first case; worth being explicit in the spec that OTHER is never used for “couldn’t evaluate” (that would need its own branch or a revert) — sounds like that’s already the intent from your writeup, just flagging it as a place implementations could quietly diverge.

Real question, not a pitch: canExecute/recordExecution answer “is this transfer permitted under the mandate’s hard caps and eligibility.” That’s a different question from “is this specific transfer sound given context” (wrong counterparty, amount right but the timing or pattern is off, principal being social-engineered) — RAMS doesn’t claim to answer that and shouldn’t. Once the reason-code enum lands, is there any interest in a registry-agnostic way to attach an optional judgment result alongside a mandate (not gating canExecute, just a citable record next to it) — or is that deliberately out of scope for this ERC and better left to compose at the executor/venue layer instead?

Coming from the map thread, where @VladKuzR said the enum is going into the next revision. One data point that might be worth having before it closes, from an enforcement layer that was written without any contact with this thread.

I maintain ERC-8370 — inheritable agent mandates, a different problem — but its enforcement gate has exactly the shape this enum describes: a boolean answer that throws away the branch that produced it. Four checks, in order, each with its own revert:

require(mandate.isActive(a.agentId),                    "agent not active");
require(mandate.payeeAllowed(a.agentId, a.payee),       "payee not allowed");
require(spent[a.agentId] + a.amount <= effectiveCap(..),"over effective cap");
require(a.amount <= room(a.agentId),                    "insufficient room");

Deployed and source-verified on Base Sepolia. Three of the four map onto your set without argument — isActive splits into AGENT_FROZEN and OUTSIDE_VALIDITY_WINDOW, payeeAllowed is close to TOKEN_NOT_ALLOWED in spirit, the cap check is OVER_CUMULATIVE_CAP.

The fourth one has no equivalent in your list, and I think that is the useful part.

insufficient room is not a cap. It is funds actually credited to this agent, minus what it has spent, minus what it has returned. An agent can be well inside its authorized ceiling and still be refused because nobody has funded it — or because it already sent the money back up its lineage. Authorization says yes; the purse is empty.

That distinction only appears once authorization and custody are separated, which RAMS also does — your registry authorizes and holds nothing. So a RAMS-aware token can hit the same condition: the mandate is valid, cumulativeUsed is well under maxCumulativeValue, and the transfer still fails on the token’s own balance or allowance. Today that would come back as OTHER, or as a plain revert with no reason at all, and a wallet cannot tell the user whether to top up or to ask for a new mandate — which is precisely the case @Anzus_GemWallet described: should I retry, or is something actually wrong? Those two have opposite answers.

Whether it belongs in the enum is your call, and there is a real argument that it does not: it is arguably the token’s condition, not the mandate’s. But if it is left out, the spec should probably say so explicitly rather than let integrators fold it into OTHER — that is the same failure the enum exists to prevent, one layer down.

On “first failing check wins”: agreed, and worth stating as a MUST rather than a SHOULD. Two implementations that report different reasons for the same call are worse than two that report nothing, because the second case is at least honestly useless.

Separately, @a-laz — reading your integration post above, we appear to have converged on the same discipline from opposite ends: publish the transaction hashes and the block-boundary reads, and let anyone recompute the cause rather than take the writeup’s word for it. That is not common, and it is good to see.

1 Like

From a user-support perspective, it would be helpful if the reasons clearly showed what the user can do next.

For example, there is an important difference between:

  • trying again later;

  • requesting a new permission;

  • reducing the amount;

  • choosing a different asset;

  • an action that only the agent or administrator can resolve.

Clear and consistent reasons would help wallets turn a failed transaction into useful guidance rather than just showing an error.

Decisions on the reason codes, in order.

The currently agreed enum is:

enum ExecutionReason {
    OK,
    NO_MANDATE,
    TOKEN_NOT_ALLOWED,
    OUTSIDE_VALIDITY_WINDOW,
    REVOKED,
    ACTION_NOT_ENABLED,
    AGENT_FROZEN,
    OVER_TX_CAP,
    OVER_CUMULATIVE_CAP,
    OTHER
}

Because ERC-8226 remains Draft, we will update canExecute() directly rather than introduce a parallel view:

function canExecute(...)
    external
    view
    returns (bool allowed, ExecutionReason reason);

On our side, the change is contained within the reference implementation under assets/erc-8226/: the registry, executor and ERC-7943 example. The only external integration documented here supports the same option.

The first failing check is a MUST, and the evaluation order becomes normative with it. Reordering an existing check is therefore a breaking change. New standardized reasons must be appended without renumbering existing values.

OTHER means that the mandate evaluation completed and rejected the action for an additional registry-defined mandate condition. A registry that cannot evaluate MUST revert, not return allowed = false. Within a pre-transfer hook, this fails closed. Turning an infrastructure failure into a policy decision would be worse than the current bare boolean.

@helmymekaoui-web, insufficient funding or room stays outside the enum, and the specification will make that boundary explicit. The registry holds no funds and cannot evaluate balances, allowances or custody. It therefore MUST NOT return OTHER for those conditions. A wallet can combine canExecute() with transaction simulation: OK followed by ERC20InsufficientBalance or ERC20InsufficientAllowance under ERC-6093 means the mandate permits the action but the venue cannot currently execute it.

@Anzus_GemWallet, the remediation categories can be described non-normatively in the Rationale:

  • NO_MANDATE or REVOKED: request a new mandate.

  • OUTSIDE_VALIDITY_WINDOW: inspect the timestamps; wait if the mandate is not yet valid, or request a new mandate if it has expired.

  • TOKEN_NOT_ALLOWED or ACTION_NOT_ENABLED: request a mandate with the required scope.

  • OVER_TX_CAP: reduce the amount.

  • OVER_CUMULATIVE_CAP: principal action is required; retrying unchanged will not help.

  • AGENT_FROZEN: the enforcer must resolve it.

  • OTHER: surface the registry’s own detail and do not infer a remedy.

@babyblueviper1, contextual judgment remains out of scope by design. canExecute() answers whether an action is authorized under the mandate, not whether that action is contextually sound. Combining those questions would make the mandate layer dependent on subjective policy that issuers could not implement or certify consistently. Any optional judgment layer should compose separately at the executor or venue level without changing RAMS authorization.

@a-laz, we agree that revocation MUST preserve the mandate record and close its authority. Deleting the record must never cause execution to fall back to a more permissive path. Aggregate exposure across principals will remain deliberately outside the mandate model because each principal controls only its own delegated risk; we will state that explicitly in the Rationale.

The remaining question is checkPrincipal(). Instead of storing only the expiresAt returned at grant time, should canExecute() re-evaluate checkPrincipal() during execution?

A stored expiry detects only time-based invalidation. A live check also detects KYC revocation, sanctions or another eligibility change occurring before expiry. The tradeoff is an additional external call and dependency on provider availability, with an evaluation failure reverting and therefore failing closed.

If adopted, we should add PRINCIPAL_INELIGIBLE as a standardized reason and place the live check explicitly in the normative evaluation order. A negative eligibility result would return that reason, while inability to evaluate would revert. @a-laz, your measured cost from the deployed integration would be particularly useful here.

Once this final point is agreed, we will open the PR with the specification, reference implementation and tests. BlueCore Studio, @VladKuzR and @a-laz will be credited for the reason-code design and integration findings. We also appreciate @helmymekaoui-web, @Anzus_GemWallet and @babyblueviper1 for the broader feedback on funding boundaries, wallet remediation and scope.

1 Like

That boundary is drawn in the right place. canExecute answering authorization-under-the-mandate and nothing else is what keeps it certifiable — an issuer can audit a closed set of authorization checks; they can’t audit “is this contextually sound,” because that question doesn’t have a closed answer. Composing at the executor/venue layer instead of inside RAMS is the same shape we use elsewhere for the same reason: keep the authority-gate’s certified surface narrow, let judgment attach beside it without being able to move the gate’s own output.

One concrete question worth asking now rather than after the PR lands: would a minimal worked example be useful — a mandate-authorized transferFrom on your live Sepolia deployment, with an independent judgment record cited alongside it (same underlying tx hash, two separately-verifiable claims, neither one able to touch canExecute’s result) — or is that better left as an exercise for adopters once the reason-code PR is in? Not proposing spec text either way, just checking whether a real end-to-end example is worth building against your existing registry/token before adoption starts, or whether that’s premature until the PRINCIPAL_INELIGIBLE question settles.

Thanks, @babyblueviper1. A worked example would be useful, and there is no need to wait for the PRINCIPAL_INELIGIBLE decision.

In the token-hook integration, RAMS assumes that the regulated asset continues enforcing its own transfer-level compliance through mechanisms such as ERC-7943’s canSend() and canReceive() or ERC-3643’s compliance hook. RAMS independently answers whether the agent is authorized under the mandate.

An implementation demonstrating the mandate check, the asset’s transfer-compliance check and the independently verifiable records around the same transaction would make that separation concrete. We would be glad to see it implemented and tested against the current Sepolia deployment; the reason-code interface can be updated once the PR lands.

Built and ran it against the live Sepolia deployment, real transactions, not synthetic: invinoveritas/examples/erc8226-three-record-composition at main · babyblueviper1/invinoveritas · GitHub

One finding worth flagging before the results: GatedUSDRams does not implement ERC-7943’s canSend()/canReceive() – confirmed via Sourcify’s verified source (supportsInterface deliberately omits IERC7943Fungible) and independently by calling both directly (both revert, no matching selector). The rationale names those as the expected asset-compliance mechanism; this live deployment exposes canTransfer (VAR sender leash) and a live checkPrincipal re-check on the mandate’s compliance provider instead. Used those as Record 2, labeled honestly rather than calling functions that don’t exist.

Two real transactions, same agent/principal/mandate:

Cleared (90 gUSD, tx 0x796a6908…ec6c): canExecute=true, canTransfer=true, checkPrincipal=eligible/COMPLIANT, /review=approve_with_concerns (0.94). Everything agrees.

Blocked (101 gUSD, tx 0xdfd1877a…1d74): canExecute=false (over the 100 gUSD tx cap), but canTransfer=true and checkPrincipal=eligible/COMPLIANT — the asset’s own checks would have allowed it. /review=reject (0.99), for the same reason RAMS gave. Zero logs on the revert; cumulativeUsed unchanged.

That’s the separation stated plainly: the asset never disagreed with the mandate on eligibility, but a value only the mandate tracks (the tx cap) is where the refusal actually came from. Independently reproducible – the README has the exact pinned cast calls (EIP-1898 hash-addressed, historical state) and both /review proofs verify fresh via /verify-proof, no need to trust our word for any of it.