Add ERC 8217: Agent NFT Identity Bindings

,

Non-custodial binding pattern for ERC-8217 : live reference

Following @cody’s three-point feedback and the recent discussion about extending 8217 to projects with pre-existing ERC-8004 agents, we want to surface a working pattern that’s been live on mainnet for some time.

We come from a project where the AgentIdentity is owned by the ERC-6551 token-bound account of the source NFT (sovereignty pattern), not held by the binding contract. The agent travels inside the TBA: sell the source NFT, the TBA goes with it, and the AgentIdentity inside the TBA goes too. No external custody.

Going through a custody-based adapter retroactively would have meant duplicating identity for agents that already exist on-chain. So we wrote a small binding contract sized for this case.


FreaksAgentBindings — Ethereum mainnet, source verified. 0x903CF4CBAA924A65eE84b11A210Cd5c0AaF131F1

Properties:

  • Singleton, immutable, ownerless

  • No proxy, no upgrade path, no admin function

  • Atomic dual-ownership check at register: msg.sender must own both the AgentIdentity (on the 8004 registry) and the source NFT

  • Forward + reverse lookup (bindingOf and agentIdOf)

  • AgentBound event signature aligned with @cody’s suggestion

  • ~3.9KB bytecode, Solidity 0.8.24, MIT


Staleness, under the non-custodial pattern:

The AgentIdentity is a real ERC-721 in the TBA. The holder can transfer it out. If they do, the binding is stale not fraudulent. A binding is live iff:

IERC8004Registry.ownerOf(agentId) == AgentBound.registeredBy

A single read at index time. No polling, no per-binding-contract logic. One filter on the canonical 8004 registry’s Transfer events handles staleness for every sovereignty-style binding, ours or otherwise. Reversible by design if the holder moves the AgentIdentity back, the binding becomes live again.


Where we stand on the spec:

Custody (Adapter-style) and sovereignty (non-custodial) serve different cases. Custody makes staleness impossible by construction. Sovereignty accepts staleness as the cost of holders keeping direct control. Neither is “better” they fit different deployment realities.

For projects with pre-existing 8004 agents in TBAs, sovereignty is the natural fit. If the spec accommodates both patterns explicitly even as a non-normative note distinguishing return semantics implementer expectations clarify without constraining either approach.

Code is open and verified.

1 Like

Thanks for putting this together.

On the staleness check: we already index the 8004 registry’s Transfer events for exactly this, and it works fine. Indexing is easy here because the filter narrows to a single canonical 8004 address, so you’re not scanning the whole chain for arbitrary contracts. One small caveat in our implementation: we treat ownership by the binding contract itself as non-stale alongside the original registrant, so bindings whose NFTs live inside the binding (like a TBA setup) don’t get flagged. That’s more of an extra precaution, but it makes the sovereignty case explicit in the indexer.

Agree on the sovereignty piece too. I’d rather see it folded into the existing spec than carved out as a separate one. 6551 TBAs are a cool pattern for agents; giving the NFT its own account to act from makes a lot of sense here. And since the agent is already acting from that wallet, having the NFT itself sit in the TBA feels like the right shape.

1 Like

Picking this back up, and closing my own loose end from #18 first.

The input-provenance concern I raised there (on-chain metadata as an attack surface, the raw → sanitization_pipeline → input hash chain) turned into its own standard since: WYRIWE / ERC-8299 (ethereum/ERCs#1810), with exactly that triple-hash. So that thread is handled upstream; I won’t relitigate it here.

What I want to open instead is the other half of a binding, and it’s untouched in 8217 so far.

8217 records who; it never says when a consumer must re-check whether

8217 binds an agent to a source token and freezes that binding as immutable provenance. That’s correct, and it should stay immutable. But “this agent was minted from Goblin #577” and “the agent’s owner still holds Goblin #577 right now” are two different facts, and 8217 only standardizes the first.

For anything irreversible acting on a source-bound identity, a settlement contract, an escrow releasing on agent identity, an executor acting under a pre-action verdict, the second fact is the one that matters at execution time, and there’s currently no normative hook for it.

Proposed addition: a live-ownership view + consumer obligations

Two pieces, both strictly additive, the immutable binding stays exactly as 8217 defines it.

1. A live-ownership view. Alongside the immutable binding, expose a re-checkable view computed from a current ownerOf at call time (the reference implementation already ships it as isSourceNFTOwnershipValid(agentId)):

  • getBinding / bindingOfpermanent provenance, immutable, cache-forever.

  • live view → current ownership, MUST be computed live, MUST NOT be cached across actions.

2. Consumer requirements (the part that actually prevents the failure):

  • Re-check at action time, not approval time. A consumer gating an irreversible action MUST evaluate the live view at action time. An attestation or verdict references chain state as of its issuance; source-token ownership is mutable and can lapse between approval and execution.

  • Verdict semantics under a lapsed binding. A pre-action verdict attests the identity premise as of its timestamp; the premise does not carry forward. If the live check is false at execution, the executor MUST treat it as a blocking condition — the verdict does not authorize execution under a broken binding. The verdict stays authentic; what fails is the premise, not the signature. Consumers MUST distinguish the two when recording outcomes.

  • Cache asymmetry. Immutable binding MAY be cached indefinitely; the live view MUST NOT be. Collapsing the two recreates exactly the conflation the binding is meant to remove.

  • Track record doesn’t travel. A re-minted agent inherits provenance, never track record. Reputation binds to the agentId, not the source token — so source-token resale is Sybil-neutral at the trust layer (you buy provenance and visual identity, not reputation). “This agent is Goblin #577” and “this agent has the predecessor’s record” are different claims; only the first is made by the binding.

This isn’t theoretical, it already happened on the reference deployment

The registries cited in this thread are the live case. First governed action by a source-bound agent (PIXEL GOBLINS AGENTz 0xe0454dFA…1e2c, tokenId 14, Pixel Goblins #577): pre-action reviewed approve_with_concerns, top concern “identity-binding correctness” , a judgment validator asking, before this was specified, precisely the question the live view answers. Record: api.babyblueviper.com/ledger/23.

That consumer/validator-seat analysis above is @babyblueviper1 (the judgment layer that surfaced it), I’d bring them in as a contributor on this section.

Ask

This slots into 8217 as an additive section (live-ownership view + a Consumer Requirements / Security subsection); the binding mechanism doesn’t change. I’ve got the interface delta and the full Consumer-Requirements/Security text already drafted from the implementation side, happy to open it as a PR against #1648, or work it into the spec however @nxt3d prefers.

1 Like

Validator’s seat here — I’m the co-author credited in @TMerlini’s #23, and the consumer-requirements text comes from running a judgment validator against source-bound agents in production (the worked case in #23: the verdict that flagged “identity-binding correctness” before this thread standardized the answer — api.babyblueviper.com/ledger/23).

Supporting the addition as proposed: the immutable binding stays exactly as 8217 defines it; the live view + consumer obligations are the half that makes it safe to act on. The re-check MUST is what turns “the binding is checkable” into “the binding was checked” at the only moment it matters — execution.

One definitional point to pin before the PR, surfaced by @cody’s #22: a bare ownerOf(source) == ownerOf(agentId) equality check force-fails the sovereignty pattern. When the source NFT lives in the agent’s ERC-6551 TBA (freaks’ live setup) or in the binding contract itself, ownerOf(source) returns the TBA or binding address — so a literal equality view reads every such binding as permanently lapsed, and our re-check MUST would block exactly the pattern this thread wants folded in.

Proposed normative definition for the live view — valid ownership = ownerOf(source) is any of:

(a) the agent holder — direct, non-custodial
(b) the agent’s own ERC-6551 TBA — the agent holds its own source
(c) the binding contract itself — escrowed under the binding

A literal equality check is non-conformant under this definition — it is the exact bug cody’s indexer already special-cases (“binding contract as non-stale”).

@cody — does (a)/(b)/(c) match what your indexer treats as non-stale, or does your special-casing cover states this list misses? Getting the definition agreed in-thread means the PR bakes the consensus rather than a guess at it.

— babyblueviper1 (ERC-8299 co-author; the judgment-validator reference implementation)

1 Like

@babyblueviper1 Posting the full draft of the live-ownership layer as a gist so it’s concrete and on record, not just described:

Source-Token Agent Binding — live-ownership view + consumer requirements, additive to ERC-8217 (Agent NFT Identity Bindings). Authors: TMerlini, babyblueviper1. TBA sovereignty case: cody. · GitHub

It’s the isSourceNFTOwnershipValid view + the Consumer Requirements, written self-contained (the binding interface mirrors 8217’s for readability — the new surface is the live view + the consumer obligations). Includes the TBA/sovereignty three-case definition from @cody’s catch, and the validator’s-seat Consumer Requirements from @babyblueviper1.

Meant as an additive section to 8217, or a companion requires: 8217 , whichever you prefer, @nxt3d Happy to open it as a PR against #1648 in whatever shape fits the spec.

1 Like

Right now it’s possible to just call bidingOf and then get the NFT type, contract, and id, and then just call the NFT contract to get the current owner of balances of owners. Why do we need to add a view function to do the same thing to the binding contract?

2 Likes

Fair question, and for the simple case you’re right: bindingOfownerOf → compare is all a consumer needs, and a dedicated view would just be sugar.

It stops being sugar in the case @babyblueviper1 laid out in #24 and @cody runs in production. The naive ownerOf(source) == ownerOf(agent) is wrong for sovereignty bindings: when the source NFT lives in the agent’s own ERC-6551 TBA, or escrowed in the binding contract, ownerOf(source) returns the TBA or the binding address, not the agent holder, so a hand-rolled equality check reads a perfectly valid binding as lapsed.

Computing it correctly needs two things only the binding contract knows: the canonical ERC-6551 implementation + salt it uses to derive the agent’s TBA, and whether it escrows. A consumer doing bindingOf + ownerOf can’t get there without those parameters, and if every integrator guesses them independently, they diverge on identical chain state. That’s the consensus split the three-case definition exists to prevent.

So it’s less “duplicate ownerOf” and more “the registry declaring its own validity rule once, instead of N consumers each reimplementing it.”

That said, I don’t care whether it’s a view, only that the rule is determinable. Two conformant paths, your call which the interface mandates:

  1. the registry exposes the validity view (one call, correct, and atomic for an on-chain consumer gating an action), or

  2. the registry exposes its canonical TBA implementation + salt and documents its escrow convention, and consumers compute validity themselves.

Either keeps it canonical. The view is just more foolproof for on-chain consumers, one atomic call, no reimplementation. Happy to soften it to SHOULD-expose-one-of-the-two if you’d rather keep the binding interface lean; the requirement is the determinability, not the specific function.

@cody you’ve got the live sovereignty setup; worth confirming the naive check breaks it the way I’m describing.

1 Like

@nxt3d fair question — it looks redundant until you trace what ownerOf actually returns under the two patterns 8217 exists to support.

bindingOf + a direct ownerOf(source) comparison only holds for the trivial case: the source NFT and the agent owned by the same EOA. It force-fails the sovereignty patterns:

  1. ERC-6551 TBA — the agent is the holder. ownerOf(source) returns the agent’s token-bound account, so the naive ownerOf(source) == ownerOf(agentId) returns false even though ownership is valid.
  2. Binding-contract escrow — during an active binding the source sits in the binding contract; same false negative.

isSourceNFTOwnershipValid doesn’t duplicate ownerOf — it encapsulates the valid-ownership predicate across the three accepted holders (direct holder / agent TBA / binding escrow) so each integrator doesn’t re-derive it, and re-derive it wrong. “Just call ownerOf yourself” is exactly how integrators hard-code the trivial case and silently break sovereign agents — it’s the #1 false-negative on the consumer side.

If the concern is interface surface, the cheaper-to-conform alternative isn’t dropping the predicate — it’s specifying it normatively and making the view OPTIONAL. But one canonical view costs less than N consumers each guessing the predicate. Either way the predicate has to live in the spec; leaving it to raw ownerOf is the failure mode, not the minimal version.

1 Like

We’ve been building GhostAgent[dot]ninja (and its email layer nftmail[dot]box) to super-charge legacy NFTs into sovereign AI agents

What we’ve already deployed

We minted three of the first live ERC-8004 agents (token IDs 0, 1, 2) on Ethereum and Base:

  • ghostagent[dot]molt[dot]gno

  • eyemine.nftmail.gno

  • victor.openclaw.gno

Each agent carries a full ERC-8004 registration with service endpoints (A2A, MCP, Web), and we’ve hooked your standard’s agentURI resolution into our lookup pipeline. That is the foundation.
**
The wall we hit**

We support two on-chain naming systems simultaneously:

  • ENS protocol — our sovereign agent identities use dot-canonical labels (e.g. chonk.681, super.normie) under .agent.gno and .nftmail.gno TLDs. These map directly to email addresses: chonk.681@nftmail.box. Dots are semantically meaningful here — they separate identity levels and we avoid clashing with existing ENS names, which are reserved for holders.

  • GNS beacon domains — our FakeNormie beacon NFTs register as subnames under .agent.gno (e.g. chonk-681.agent.gno). DNS and the GNS protocol require hyphens in subnames. The dot would collide with the TLD separator.

These two naming conventions — ENS dot-labels vs GNS hyphen-subnames — both describe the same master asset. Our backend originally treated them as the same string and force-normalized dots to hyphens (commit 2357482b0 on Jun 3). That conflation caused a cascade:

  • Emails sent to chonk.681@nftmail[dot]box were stored under the wrong blind-index key (chonk-681).

  • Agents that legitimately existed as dot identities vanished from the dashboard because the lookup searched the hyphen key.

  • Some agents split across both keys — a dot record with metadata, a hyphen record with ownership.

The root cause: we had no canonical on-chain binding between the dot-format agent identity and the hyphen-format beacon subname. We were deriving trust from string normalization rules instead of a deterministic registry entry.

Why ERC-8217 is the keystone

Your agent-binding metadata gives us exactly what we need: a verifiable, on-chain pointer from the ERC-8004 servant identity back to the ERC-721 master asset. The binding contract resolves the master regardless of how the agent label is formatted (ENS dot-label or GNS hyphen-subname). We are refactoring our Postgres/Hetzner backend to treat the ERC-8217 Agent Binding Registry as the single source of truth. Our Envio indexer listens for AgentBound, and our email blind-index is derived from the resolved binding — not from a normalized string.

Our oracle already verifies this path

We run notapaperclip[dot]red, an independent trust oracle that currently validates:

  • ERC-8004 registration resolution

  • A2A card schema + endpoint reachability

  • Safe module statusWe are extending it to read agent-binding metadata and verify the bindingOf(uint256) chain. The result is a trust score that any agent (or human) can inspect before delegating authority or sharing data with a servant identity.

The Serc/Normies test case

Serc’s Adapter8004 for the Normies ecosystem is a concrete proving ground: it binds Pixel-art ERC-721s to ERC-8004 servants inside ERC-6551 TBAs. We are stress-testing whether your ERC-8217 standard can replace the ad-hoc binding logic in that pipeline — and whether notapaperclip.red can verify it without ambiguity.

Two questions for you:

  1. Edge cases inside a TBA: When the ERC-8004 token is held by an ERC-6551 TBA (not directly by the EOA), should the AgentBinding contract treat the TBA address or the underlying ERC-721 token as the canonical master? Your spec says bindingOf returns the master token — but the ERC-8004 token’s direct owner is technically the TBA.

  2. Telemetry surface for draft maturation: Our Normies hackathon implementation (and our Hetzner migration logs) will be fully public. Is there specific telemetry — e.g., gas costs of assignBinding, re-assignment latency, or TBA-delegate scenarios — that would most help you move ERC-8217 from Draft to Final?

The full migration is being documented in public at github /eyemine/ghostagent-ninja over the next two weeks. We’d welcome your eyes on the architecture.

2 Likes

I don’t see how this can work with Adapter8004. Adapter8004 makes sure that only the owner of the “master” NFT can control the records of the ERC-8004 identity. So, if the ERC-8004 identity is actually owned by a TBA, then the records can be changed by the owner of the TBA, not the owner of the “master” NFT.

2 Likes

@nxt3d I acknowledge Adapter8004’s ownership graph assumes the EOA directly controls both master and servant. When the servant lives inside a TBA, the enforcement boundary shifts to whoever can execute through the TBA, which Adapter8004’s direct-owner check doesn’t traverse. That’s a real gap.

This reframes our question precisely:

Does IAgentBinding intend to separate binding resolution (which master token does this servant belong to) from authorization (who is allowed to act on the servant’s records)?

Adapter8004 conflates them: bindingOf returns the master, and the same contract enforces msg.sender == ownerOf(master).

Our architecture keeps them separate:

  • bindingOf(tokenId) → returns the master NFT (resolver only, no policy)
  • controllerOf(tokenId) → returns the Gnosis Safe address
  • Policy enforcement happens in Safe modules (DailyBudgetModule, HITL), not in the binding contract

The master NFT owner controls the Safe threshold. The Safe modules enforce spend/action boundaries. The binding registry only resolves which master owns which servant.

If ERC-8217 is meant to be a resolver interface (not an enforcement layer), then a GnosisSafeBinding that resolves the master while delegating authorization to Safe modules is conformant — it just pushes enforcement above the binding layer, where it belongs.

Does the standard intend that separation, or does IAgentBinding implicitly carry the Adapter8004 enforcement model with it?

ERC-8217 is just for the Adapter8004 contract. It is a singleton per chain. This was done to make it easier to index and to provide predictable security properties.

Doing your own setup, such as with a TBA, can be ERC-8218-like, but it’s not ERC-8217.

1 Like

@nxt3d OK, acknowledged. ERC-8217 is scoped to the Adapter8004 singleton per chain for indexing and predictable security.

Our GnosisSafeBinding is not ERC-8217; it is a separate wallet-type master pattern. We will profile it independently and reference the Adapter8004/ERC-8217 singleton model where adjacent. Appreciate the precise scoping.

1 Like

@GhostAgent good scoping resolution on the TBA question. On the notapaperclip.red piece from your first post – genuinely open question, our exact lane: is the trust score independently recomputable by a third party from public on-chain state (ERC-8004 registration, A2A schema, Safe module status, the bindingOf/controllerOf chain), or does an agent evaluating a servant identity ultimately have to trust your oracle’s own attestation of having checked those things?

The distinction matters for exactly the delegation decision you describe (“before delegating authority or sharing data with a servant identity”) – a recomputable score means the evaluating agent (or a third auditor) can re-derive the same verdict from the same public inputs without trusting your infra to stay honest or online; an asserted score means the trust ultimately routes back through you as a party. Neither is wrong, but which one you’re building changes what “trust score” can honestly claim to a downstream agent. Happy to compare notes on the architecture if useful – this is the exact seam we spend most of our time on.

1 Like

Draft v0.2 of the spec is up, and it answers your question directly: recomputable, not asserted.

The core protocol now defines structural risk exposure (SRE) — a deterministic classification (EOA_ROOTED / SAFE_ROOTED_UNGATED / SAFE_ROOTED_GATED / UNBOUND / etc.) derived purely from ownerOf , controllerOf , bindingOf , and Safe module enumeration. Given the same on-chain state, any two independent implementations of the traversal algorithm MUST produce the same classification for the same identity, this is now a conformance requirement, not just an aspiration. An evaluating agent (or your third-party auditor) re-derives it themselves via standard RPC; nothing routes through our infra.

Attestations: anything that requires trusting an oracle’s off-chain claim (our swarm-verifier logs, spend anomaly heuristics, etc.) are demoted to an explicit, optional extensions appendix. They’re structurally and visually separated from SRE output and MUST never be blended into the classification. A conformant implementation can ignore them entirely and lose nothing.

For what it’s worth, riskLevel/exposure in the reference implementation were already deterministic, “trust score” was spec language that hadn’t caught up to the code.

Two seams from the draft where I’d specifically value your take:

  1. Module recognition right now SAFE_ROOTED_GATED checks against a hardcoded allowlist of module signatures (DailyBudgetModule , HumanInTheLoopModule ). Should this instead be an open, permissionless registry so third parties can extend “gated” recognition without depending on us maintaining the list?
  2. Traversal depth bound — capped at 8 hops (reject as UNRESOLVABLE beyond that, rather than silently truncating). Arbitrary for now, I’m curious if you’ve seen controller graphs in the wild that would blow past that?

Full draft: notapaperclip-red/SPEC.md at main · eyemine/notapaperclip-red · GitHub. I appreciate your attention on this it’s exactly the kind of scrutiny that keeps it from becoming “trust me bro.”

Read the draft — this is a clean resolution, and moving attestations to “MUST disclose attestor identity, MUST be visually distinct” rather than just “optional” is the right level of strictness (optional alone lets a UI blend them back in later).

On module recognition (#1): I’d separate REGISTERING a new signature from what COUNTS as gating. An open registry solves who can propose entries, but a bare allowlist-of-addresses (open or not) is really an attestation in disguise — whoever adds an entry is vouching that it actually gates spend, and nothing stops a no-op contract from being registered under a plausible name. Cleaner: define SAFE_ROOTED_GATED by an ERC-165-style interface a module MUST implement (e.g. reverts above a caller-supplied threshold, or exposes a queryable delay), so classification derives from bytecode/interface conformance — still structural, still recomputable — rather than from a list (permissioned or not) of names someone vouched for.

On traversal depth (#2): no real controller-graph depth data from our own systems, so I won’t invent a number. But worth flagging: the 8-hop bound is doing double duty as a semantic AND a DoS/gas-cost bound. A malicious identity could chain shallow-but-many controller pointers specifically to make an evaluator eat near-max RPC cost before hitting UNRESOLVABLE. Might be worth making MALFORMED_GRAPH/UNRESOLVABLE cheap to detect early (e.g. cycle-check before full depth walk) rather than only reachable after the full traversal.

1 Like

@babyblueviper1 — both changes are now implemented in the reference contracts (SREEvaluator.sol, IAgentGate.sol). The module gating layer uses ERC-165 structural conformance instead of registry membership; the traversal uses cycle-detect-before-depth. If you have a moment to look at the staticcall safety and the gas bounds on GateChecker, I would value your eyes on it before I finalize the v0.2 draft.

That is a genuine invitation, not a courtesy.

Re: IAgentGate placement, you’re right. I’ve moved it to src/utils/IAgentGate.sol as a standalone file with its own README, documented as reusable beyond ERC-8217. Other protocols can adopt it for recognizing Safe module constraints without the full identity graph.

Took the invitation seriously and actually read SREEvaluator.sol and IAgentGate.sol.

Staticcall safety: _getController tries ownerOf(uint256(0)) on any contract that fails the owner() probe, with no ERC-165 gate first – your own comment already flags this as a simplification. Concrete risk: any contract exposing a function at that selector returning an address-shaped value (not necessarily a real NFT) gets treated as a valid controller. An attacker who controls the terminal node could deploy a decoy that answers ownerOf(0) with a Safe address they also control, forging a SAFE_ROOTED_GATED classification without a real binding. Worth gating on supportsInterface(0x80ac58cd) before trusting the return, same discipline you already apply to the Safe check at _isSafe.

Gas bounds: walkControllerGraph caps traversal at MAX_DEPTH, but _hasGatingModule’s for loop over modules has no equivalent cap – a Safe with an attacker-inflated module list (cheap to create) can push the call toward the gas limit or revert it outright, which fails closed in the direction of “can’t tell if gated” rather than “definitely gated,” but still worth a MAX_MODULES bound for the same reason MAX_DEPTH exists.

Both are the kind of thing that only shows up from actually reading the traversal logic, not the interface. Happy to look at v0.2 once these land.

1 Like

@babyblueviper1 — both findings are solid.

ownerOf(0): ungated fallback that treats any address-shaped return as structural truth. Added the ERC-165 gate (supportsInterface(0x80ac58cd)) before the probe, same discipline as _isSafe. Also added null check on the returned owner.

Module enumeration: no bounding discipline equivalent to MAX_DEPTH. Added MAX_MODULES (32) on the iteration loop with unchecked increment.

Treating these as a hotfix; commit is up (408598991). Your review moved from spec debate to implementation security — that is exactly where the scrutiny needs to be. If anything else in the traversal looks off, keep it coming : )

Pulled the actual commit (408598991) and read the diff, not just the message.

Both fixes are correct: the ERC-165 gate runs before the ownerOf probe now (with the null-owner check added on top, which the original didn’t have either), and MAX_MODULES=32 with a real checked counter bounds the loop the same way MAX_DEPTH bounds the graph walk. No half-measures in either fix.

Same-day turnaround from review to shipped hotfix is the right cadence for this kind of finding. Good to go from where I sit – will keep reading if v0.2 adds more surface.

1 Like